"""
Minimal encoder/decoder for the Mihon 0.20.1+ extension-repo protobuf format
(the `index.pb` served by Keiyoushi and consumed by Mihon).

Reverse-engineered wire schema:

  Repo {
    1   string name
    2   string badgeLabel
    3   string signingKey            // hex sha256 of APK signing cert
    4   Contact contact { 1 string website; 2 string discord }
    101 ExtensionList extensions { 1 repeated Extension extension }
  }

  Extension {
    1 string name
    2 string packageName
    3 Resources resources { 1 string apkUrl; 2 string iconUrl; 501 string jarUrl }
    4 string extensionLib           // "1.4" / "1.6"
    5 uint64 versionCode
    6 string versionName
    7 ContentWarning contentWarning
    8 repeated Source sources {
        1 uint64 id; 2 string name; 3 string language; 4 string homeUrl;
        5 repeated string mirrorUrls
      }
  }

  ContentWarning: UNSPECIFIED=0 SAFE=1 MIXED=2 NSFW=3

The whole message is gzip-compressed on disk.
"""

import gzip

CW_TO_INT = {
    None: 0,
    "CONTENT_WARNING_UNSPECIFIED": 0,
    "CONTENT_WARNING_SAFE": 1,
    "CONTENT_WARNING_MIXED": 2,
    "CONTENT_WARNING_NSFW": 3,
}
INT_TO_CW = {v: k for k, v in CW_TO_INT.items() if k}
INT_TO_CW[0] = "CONTENT_WARNING_UNSPECIFIED"


# ---------------------------------------------------------------- encoding

def _varint(n):
    out = bytearray()
    while True:
        b = n & 0x7F
        n >>= 7
        if n:
            out.append(b | 0x80)
        else:
            out.append(b)
            return bytes(out)


def _tag(field, wire):
    return _varint((field << 3) | wire)


def _str(field, value):
    if value is None or value == "":
        return b""
    raw = value.encode("utf-8")
    return _tag(field, 2) + _varint(len(raw)) + raw


def _msg(field, raw):
    if not raw:
        return b""
    return _tag(field, 2) + _varint(len(raw)) + raw


def _uint(field, value):
    if not value:
        return b""
    return _tag(field, 0) + _varint(int(value))


def encode_repo(meta, extensions):
    """meta: dict(name, badgeLabel, signingKey, contact{website,discord})
    extensions: list of dicts in the index.json schema."""
    out = bytearray()
    out += _str(1, meta.get("name"))
    out += _str(2, meta.get("badgeLabel"))
    out += _str(3, meta.get("signingKey"))

    contact = meta.get("contact") or {}
    c = _str(1, contact.get("website")) + _str(2, contact.get("discord"))
    out += _msg(4, c)

    lst = bytearray()
    for e in extensions:
        lst += _msg(1, encode_extension(e))
    out += _msg(101, bytes(lst))
    return bytes(out)


def encode_extension(e):
    out = bytearray()
    out += _str(1, e.get("name"))
    out += _str(2, e.get("packageName"))

    r = e.get("resources") or {}
    res = _str(1, r.get("apkUrl")) + _str(2, r.get("iconUrl")) + _str(501, r.get("jarUrl"))
    out += _msg(3, res)

    out += _str(4, e.get("extensionLib"))
    out += _uint(5, e.get("versionCode"))
    out += _str(6, e.get("versionName"))
    out += _uint(7, CW_TO_INT.get(e.get("contentWarning"), 0))

    for s in e.get("sources") or []:
        sub = (_uint(1, s.get("id")) + _str(2, s.get("name"))
               + _str(3, s.get("language")) + _str(4, s.get("homeUrl")))
        for m in s.get("mirrorUrls") or []:
            sub += _str(5, m)
        out += _msg(8, sub)
    return bytes(out)


def write_index_pb(path, meta, extensions):
    raw = encode_repo(meta, extensions)
    # mtime=0 so repeated runs with identical content produce identical files
    with open(path, "wb") as fh:
        with gzip.GzipFile(fileobj=fh, mode="wb", mtime=0) as gz:
            gz.write(raw)
    return len(raw)


# ---------------------------------------------------------------- decoding

def _rv(b, i):
    r = 0
    s = 0
    while True:
        x = b[i]
        i += 1
        r |= (x & 0x7F) << s
        if not (x & 0x80):
            return r, i
        s += 7


def _fields(b):
    i = 0
    while i < len(b):
        key, i = _rv(b, i)
        fn, wt = key >> 3, key & 7
        if wt == 0:
            v, i = _rv(b, i)
            yield fn, v
        elif wt == 2:
            ln, i = _rv(b, i)
            yield fn, b[i:i + ln]
            i += ln
        elif wt == 5:
            i += 4
        elif wt == 1:
            i += 8
        else:
            raise ValueError("bad wire type %d" % wt)


def decode_repo(raw):
    if raw[:2] == b"\x1f\x8b":
        raw = gzip.decompress(raw)
    meta = {"contact": {}}
    exts = []
    for fn, v in _fields(raw):
        if fn == 1:
            meta["name"] = v.decode()
        elif fn == 2:
            meta["badgeLabel"] = v.decode()
        elif fn == 3:
            meta["signingKey"] = v.decode()
        elif fn == 4:
            for cf, cv in _fields(v):
                meta["contact"]["website" if cf == 1 else "discord"] = cv.decode()
        elif fn == 101:
            for ef, ev in _fields(v):
                if ef == 1:
                    exts.append(decode_extension(ev))
    return meta, exts


def decode_extension(b):
    e = {"resources": {}, "sources": []}
    for fn, v in _fields(b):
        if fn == 1:
            e["name"] = v.decode()
        elif fn == 2:
            e["packageName"] = v.decode()
        elif fn == 3:
            for rf, rv_ in _fields(v):
                key = {1: "apkUrl", 2: "iconUrl", 501: "jarUrl"}.get(rf)
                if key:
                    e["resources"][key] = rv_.decode()
        elif fn == 4:
            e["extensionLib"] = v.decode()
        elif fn == 5:
            e["versionCode"] = str(v)
        elif fn == 6:
            e["versionName"] = v.decode()
        elif fn == 7:
            e["contentWarning"] = INT_TO_CW.get(v)
        elif fn == 8:
            s = {}
            for sf, sv in _fields(v):
                if sf == 1:
                    s["id"] = str(sv)
                elif sf == 2:
                    s["name"] = sv.decode()
                elif sf == 3:
                    s["language"] = sv.decode()
                elif sf == 4:
                    s["homeUrl"] = sv.decode()
                elif sf == 5:
                    s.setdefault("mirrorUrls", []).append(sv.decode())
            e["sources"].append(s)
    return e
