import struct
import zlib
import re
import io
import os

from dataclasses import dataclass

from Crypto.Cipher import AES


SOURCE = "./kcf"
OUT = "templates/pages/sega/software/security/"

APPDATA_BINARY_LEN = 288  # RingEdge
APPDATA_BINARY_LEN2 = 416  # RingEdge 2
APPDATA_NU_BINARY_LEN = 112  # Nu

FUNCTION_TYPE = {
    1: "Server (SV)",
    2: "Satellite (ST)",
    3: "Live (LV)",
    4: "Terminal (??)",
    5: "UNKNOWN:5",
    11: "UNKNOWN:11",
    12: "UNKNOWN:12",
    21: "UNKNOWN:21",
    22: "UNKNOWN:22",
    98: "Standalone (??)",
}
PLATFORM = {
    "AAM": "RingWide",
    "AAL": "RingEdge",
    "AAS": "RingEdge 2",
    "AAR": "ELEFUN",

    "AAV": "Nu",
    "AAZ": "Nu Lv 3.1",
    "AAW": "Nu SX",
    "ACA": "ALLS X",
    "ACB": "ALLS X2",
}

SF_DEVELOP = 1 << 0
SF_ALL_NET = 1 << 2
SF_DELIVER = 1 << 3
SF_BINDING = 1 << 4
SF_BILLING = 1 << 5
SF_RENTAL = 1 << 6


class Stream:
    def __init__(self, stream):
        self.stream = stream

    def read(self, n):
        return self.stream.read(n)

    def seek(self, n):
        self.stream.seek(n)

    def crc32(self, nbytes):
        start = self.stream.tell()
        data = self.stream.read(nbytes)
        self.stream.seek(start)
        return zlib.crc32(data) & 0xffffffff

    def skip(self, n):
        self.read(n)

    def unpack(self, fmt):
        return struct.unpack("<" + fmt, self.stream.read(struct.calcsize(fmt)))

    def u8(self):
        return self.unpack("B")[0]

    def u16(self):
        return self.unpack("H")[0]

    def u32(self):
        return self.unpack("I")[0]

    def ipv4(self):
        return self.unpack("4B")

    def ipv6(self):
        return self.unpack("8H")

    def str(self, n):
        return self.unpack(f"{n}s")[0].decode("latin-1")


class AppData:
    system_flag: int
    function_type: int
    region: int
    platform_id: str

    def parse_region(self):
        if self.region == 0xFF:
            return "ALL"
        regions = []
        if self.region & 1:
            regions.append("JPN")
        if self.region & 2:
            regions.append("USA")
        if self.region & 4:
            regions.append("EXP")
        if self.region & 8:
            regions.append("CHN")
        if self.region & ~(1 | 2 | 4 | 8):
            regions.append(f"ex:{self.region}")
        return "/".join(regions)

    def parse_system_flag(self):
        """
        [bit 0] Develop:
            1: Key chip for development
            0: Key chip for mass production

        [bit 2] ALL.Net:
            1: ON (use)
            0: OFF (do not use)

        [bit 3] Deliver (LAN installation):
            1: Local FDC Server
            0: Client
            Ethernet network: Used & Mode: 1 only when server

        [bit 4] Binding:
            1: ON
            0: OFF
            * Not used in ELEFUN (= 0) (cannot be set as = 1)

        [bit 5] Billing:
            Requires ALL.Net to be ON
            1: ON
            0: OFF (no charge)
            * Not used in ELEFUN (= 0) (cannot be set as = 1)

        [bit 6] Rental (P-ras charging):
            Requires ALL.Net to be ON
            Requires Billing to be ON
            * Not used in ELEFUN (= 0) (cannot be set as = 1)

            1: P-ras billing (rental billing)
            0: pay-as-you-go billing (net billing)
        """

        parsed = []
        if self.system_flag & 1:
            parsed.append("Develop")
        if self.system_flag & 2:
            parsed.append("2")
        if self.system_flag & 4:
            parsed.append("ALL.Net")
        if self.system_flag & 8:
            parsed.append("Deliver")
        if self.system_flag & 16:
            parsed.append("Binding")
        if self.system_flag & 32:
            parsed.append("Billing")
        if self.system_flag & 64:
            parsed.append("Rental")
        if self.system_flag & 128:
            parsed.append("128")
        return ",".join(parsed)

    def parse_function_type(self):
        return FUNCTION_TYPE[self.function_type]

    def parse_platform_id(self):
        return f"{PLATFORM[self.platform_id]} ({self.platform_id})"


@dataclass
class AppDataRing(AppData):
    # Appboot
    format_type: int
    game_id: str
    region: int
    function_type: int
    system_flag: int
    platform_id: str
    dvd_flag: int
    network_addr: tuple[int]

    # Misc
    app_data: bytes

    # Crypto
    key: bytes
    iv: bytes
    seed: bytes
    keyfile: bytes

    # Misc (2)
    extra: bytes


@dataclass
class AppDataNu(AppData):
    # Appboot
    format_type: int
    game_id: str
    region: int
    function_type: int
    system_flag: int
    billing_form: int
    platform_id: str
    network_addr: tuple[int]
    network_addr_6: tuple[int]

    # Crypto
    gkey: bytes
    giv: bytes
    okey: bytes
    oiv: bytes


def scramble(val, a, b, c, d):
    ret = bytearray(val)
    ret[a], ret[b] = ret[b], ret[a]
    ret[c], ret[d] = ret[d], ret[c]
    return bytes(ret)


def parse_appdata_ring(stream: Stream):
    crc = stream.u32()  # idk how to make this pass. it doesn't match?

    format_type = stream.u8()
    stream.skip(3)
    game_id = stream.str(4)
    region = stream.u8()
    function_type = stream.u8()
    system_flag = stream.u8()
    stream.skip(1)
    platform_id = stream.str(3)
    dvd_flag = stream.u8()
    network_addr = stream.ipv4()
    app_data = stream.read(216)
    seed = scramble(stream.read(16), 1, 8, 12, 15)
    key = scramble(stream.read(16), 0, 4, 2, 14)
    iv = scramble(stream.read(16), 0, 11, 5, 15)

    # AAL, AAS, AAR has:
    extra = stream.read(128)
    # For AAR, extra=FF....FF

    # if extra and platform_id != 'AAR':
    #     print(extra[:64].hex(), platform_id, game_id, function_type)

    cipher = AES.new(key, AES.MODE_CBC, iv)
    keyfile = cipher.encrypt(seed)

    return AppDataRing(
        format_type, game_id, region, function_type, system_flag,
        platform_id, dvd_flag, network_addr, app_data,
        key, iv, seed, keyfile, extra
    )


def parse_appdata_nu(stream: Stream):
    crc = stream.u32()

    format_type = stream.u8()
    stream.skip(3)
    game_id = stream.str(4)
    region = stream.u8()
    function_type = stream.u8()
    system_flag = stream.u8()
    billing_form = stream.u8()
    platform_id = stream.str(3)
    stream.skip(1)
    network_addr = stream.ipv4()
    stream.skip(8)
    ipv6_addr = stream.ipv6()
    gkey = stream.read(16)
    giv = stream.read(16)
    okey = stream.read(16)
    oiv = stream.read(16)

    return AppDataNu(
        format_type, game_id, region, function_type, system_flag,
        billing_form, platform_id, network_addr, ipv6_addr,
        gkey, giv, okey, oiv
    )


def format_row(*values, elem="td"):
    return "<tr>" + "".join(f"<{elem}>{i}</{elem}>" for i in values) + "</tr>"


def format_code(code):
    return f"<code>{code}</code>"


def parse_kcf(data):
    kcf = io.BytesIO(data)

    nbytes = len(data)

    if nbytes == APPDATA_BINARY_LEN:
        return parse_appdata_ring(Stream(kcf)), None
    elif nbytes == APPDATA_BINARY_LEN2:
        return parse_appdata_ring(Stream(kcf)), None
    elif nbytes == APPDATA_NU_BINARY_LEN:
        return None, parse_appdata_nu(Stream(kcf))
    else:
        raise ValueError


STYLES = """
<style>
body {
    font-family: sans-serif;
}
table {
    white-space: nowrap;
    overflow-x: auto;
    border-collapse: collapse;
}
th {
    border-bottom: 3px solid #000;
    position: sticky;
    top: 0;
    text-align: left;
    background: #fff;
}
td {
    border-bottom: 1px solid #ddd;
}
th, td {
    border-left: 1px solid #ddd;
}
th:last-child, td:last-child {
    border-right: 1px solid #ddd;
}
th, td {
    margin: 0;
    padding: 2px 4px;
}
</style>
"""


def gen_file_ring(path: str, platform: str, appdata: list[AppDataRing]):
    with open(path, "w") as kcf_html:
        print(STYLES, file=kcf_html)
        print("<table><thead>", file=kcf_html)
        print(format_row(
            "Game ID", "Region", "Model Type",
            "Develop Flag", "ALL.Net", "Deliver", "Binding", "Billing", "Rental",
            "DVD Enabled", "Platform ID", "IPv4 Subnet",
            "AES Key", "AES IV", "AppBoot Seed", "Keyfile",
            "App Data", "Trailing Data",
            elem="th"
        ), file=kcf_html)
        print("</thead></tbody>", file=kcf_html)
        appdata.sort(key=lambda x: (x.platform_id, x.network_addr, x.game_id, x.format_type, x.system_flag))

        for i in appdata:
            if i.platform_id != platform:
                continue

            print(format_row(
                i.game_id,
                i.parse_region(),
                i.parse_function_type(),
                "X" if i.system_flag & SF_DEVELOP else "",
                "X" if i.system_flag & SF_ALL_NET else "",
                "X" if i.system_flag & SF_DELIVER else "",
                "X" if i.system_flag & SF_BINDING else "",
                "X" if i.system_flag & SF_BILLING else "",
                "X" if i.system_flag & SF_RENTAL else "",
                "X" if i.dvd_flag else "",
                i.parse_platform_id(),
                ".".join(map(str, i.network_addr)),
                format_code(i.key.hex().upper()),
                format_code(i.iv.hex().upper()),
                format_code(i.seed.hex().upper()),
                format_code(i.keyfile.hex().upper()),
                ""
                if all(j == 0 for j in i.app_data)
                else format_code(i.app_data.hex().upper()),
                format_code(i.extra.hex().upper()),
            ), file=kcf_html)
        print("</tbody></table>", file=kcf_html)


def main():
    appdata_ring: list[AppDataRing] = []
    appdata_nu: list[AppDataNu] = []

    raw = set()
    for i in os.listdir(SOURCE):
        raw.add(open(os.path.join(SOURCE, i), "rb").read())

    for i in raw:
        ring, nu = parse_kcf(i)

        if ring:
            appdata_ring.append(ring)
        if nu:
            appdata_nu.append(nu)

    gen_file_ring(OUT + "kcf-ringedge.html", "AAL", appdata_ring)
    gen_file_ring(OUT + "kcf-ringwide.html", "AAM", appdata_ring)
    gen_file_ring(OUT + "kcf-ringedge2.html", "AAS", appdata_ring)
    gen_file_ring(OUT + "kcf-elefun.html", "AAR", appdata_ring)

    with open(OUT + "kcf-nu.html", "w") as kcf_nu:
        print(STYLES, file=kcf_nu)
        print("<table><thead>", file=kcf_nu)
        print(format_row(
            "Game ID", "Region", "Model Type",
            "Develop Flag", "ALL.Net", "Deliver", "Binding", "Billing", "Rental",
            "Billing Form", "Platform ID",
            "IPv4 Subnet", "IPv6 Subnet",
            "Game Key", "Game IV", "Opt Key", "Opt IV",
            elem="th"
        ), file=kcf_nu)
        print("</thead></tbody>", file=kcf_nu)
        appdata_nu.sort(key=lambda x: (x.platform_id, x.network_addr, x.game_id, x.format_type, x.system_flag))

        for i in appdata_nu:
            ipv6 = ":".join(f"{j:04x}" for j in i.network_addr_6)
            ipv6 = re.sub(r"/(^(0000:?)+)|(:(0000:)+)|((:0000)+$)/", "::", ipv6)

            print(format_row(
                i.game_id,
                i.parse_region(),
                i.parse_function_type(),
                "X" if i.system_flag & SF_DEVELOP else "",
                "X" if i.system_flag & SF_ALL_NET else "",
                "X" if i.system_flag & SF_DELIVER else "",
                "X" if i.system_flag & SF_BINDING else "",
                "X" if i.system_flag & SF_BILLING else "",
                "X" if i.system_flag & SF_RENTAL else "",
                i.billing_form,
                i.parse_platform_id(),
                ".".join(map(str, i.network_addr)),
                ipv6,
                format_code(i.gkey.hex().upper()),
                format_code(i.giv.hex().upper()),
                format_code(i.okey.hex().upper()),
                format_code(i.oiv.hex().upper()),
            ), file=kcf_nu)
        print("</tbody></table>", file=kcf_nu)


if __name__ == "__main__":
    main()
