#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
from pathlib import Path
import re
import subprocess


SOURCE_NAMES = (
    "compress.h",
    "decompressor.c",
    "decompressor_lz4.c",
    "decompressor_lzma.c",
    "decompressor_deflate.c",
    "decompressor_zstd.c",
    "zdata.c",
)


def fail(message: str) -> None:
    raise SystemExit(message)


def replace_once(source: str, old: str, new: str, label: str) -> str:
    count = source.count(old)
    if count != 1:
        fail(f"{label}: expected one source occurrence, found {count}")
    return source.replace(old, new, 1)


def extract_function(source: str, name: str) -> str:
    match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
    if not match:
        fail(f"missing function: {name}")
    name_line = source.rfind("\n", 0, match.start()) + 1
    start = source.rfind("\n", 0, name_line - 1) + 1
    brace = source.find("{", match.end())
    if brace < 0:
        fail(f"missing function body: {name}")
    depth = 0
    state = "code"
    index = brace
    while index < len(source):
        char = source[index]
        following = source[index + 1] if index + 1 < len(source) else ""
        if state == "code":
            if char == "/" and following == "*":
                state = "block"
                index += 2
                continue
            if char == "/" and following == "/":
                state = "line"
                index += 2
                continue
            if char == '"':
                state = "string"
            elif char == "'":
                state = "character"
            elif char == "{":
                depth += 1
            elif char == "}":
                depth -= 1
                if depth == 0:
                    return source[start : index + 1]
        elif state == "block" and char == "*" and following == "/":
            state = "code"
            index += 2
            continue
        elif state == "line" and char == "\n":
            state = "code"
        elif state in {"string", "character"}:
            if char == "\\":
                index += 2
                continue
            if (state == "string" and char == '"') or (
                state == "character" and char == "'"
            ):
                state = "code"
        index += 1
    fail(f"unterminated function: {name}")


def committed(root: Path, baseline: str, name: str) -> str:
    completed = subprocess.run(
        ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{name}"],
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    if completed.returncode != 0:
        fail(f"cannot read B25 baseline {name}: {completed.stderr.strip()}")
    return completed.stdout


def require_once(source: str, needle: str, label: str) -> None:
    count = source.count(needle)
    if count != 1:
        fail(f"{label}: expected one occurrence, found {count}")


def verify_exact_small_transforms(current: dict[str, str], base: dict[str, str]) -> None:
    expected = replace_once(
        base["compress.h"],
        "\tconst char *name;\n",
        "\tconst char *name;\n"
        "\t/* Callbacks return zero or a positive FreeBSD errno. */\n",
        "callback errno contract",
    )
    if current["compress.h"] != expected:
        fail("compress.h differs from the declared callback contract annotation")

    expected = base["decompressor.c"]
    expected = replace_once(
        expected,
        "\t\tif (size < sizeof(*lz4))\n\t\t\treturn (EINVAL);",
        "\t\tif (size < sizeof(*lz4))\n\t\t\treturn (EINTEGRITY);",
        "short LZ4 config",
    )
    expected = replace_once(
        expected,
        "\t\t    (Z_EROFS_PCLUSTER_MAX_SIZE >> sbi->blkszbits))\n"
        "\t\t\treturn (EINVAL);",
        "\t\t    (Z_EROFS_PCLUSTER_MAX_SIZE >> sbi->blkszbits))\n"
        "\t\t\treturn (EOPNOTSUPP);",
        "unsupported LZ4 pcluster size",
    )
    if expected.count("\t\treturn (EOVERFLOW);\n") != 2:
        fail("baseline config arithmetic errno count changed")
    expected = expected.replace(
        "\t\treturn (EOVERFLOW);\n", "\t\treturn (EINTEGRITY);\n"
    )
    expected = replace_once(expected, "\tint ret;\n", "", "folding temporary")
    expected = replace_once(
        expected,
        "\tret = decompressor->decompress(&rq);\n"
        "\treturn (ret == 0 ? 0 : EIO);",
        "\treturn (decompressor->decompress(&rq));",
        "dispatch preservation",
    )
    if current["decompressor.c"] != expected:
        fail("decompressor.c differs from the declared B25 dispatch/config transforms")

    expected = base["decompressor_lz4.c"]
    if expected.count("return (-1);") != 12 or expected.count(": -1);") != 1:
        fail("baseline LZ4 private-status count changed")
    expected = expected.replace("return (-1);", "return (EINTEGRITY);")
    expected = expected.replace(": -1);", ": EINTEGRITY);")
    if current["decompressor_lz4.c"] != expected:
        fail("LZ4 differs from the exact private-status conversion")

    if current["zdata.c"] != base["zdata.c"]:
        fail("B25 changed the already-correct zdata I/O or cleanup path")


def verify_source_contracts(
    source: dict[str, str], fixture: dict[str, object]
) -> dict[str, object]:
    joined = "\n".join(source[name] for name in SOURCE_NAMES)
    if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", joined):
        fail("negative Linux errno entered the FreeBSD codec path")
    for name in SOURCE_NAMES:
        if "return (-1)" in source[name] or re.search(r":\s*-1\s*\)", source[name]):
            fail(f"private -1 codec status remains in {name}")

    dispatch = extract_function(source["decompressor.c"], "z_erofs_decompress")
    if dispatch.count("return (decompressor->decompress(&rq));") != 2:
        fail("typed backend dispatch does not have two direct callback exits")
    if "ret == 0 ? 0 : EIO" in dispatch:
        fail("dispatcher still folds typed backend errors")
    require_once(dispatch, "return (EOPNOTSUPP);", "unsupported algorithm")
    if dispatch.count("return (EINTEGRITY);") != 2:
        fail("dispatch-local corruption checks changed")
    for case in fixture["dispatch"]:
        if case.get("backend_errno") != case.get("expected"):
            fail("dispatch fixture does not require exact errno preservation")

    config_expectations = {
        "z_erofs_load_lz4_config": ("decompressor.c", "EINTEGRITY", "EOPNOTSUPP"),
        "z_erofs_load_lzma_config": (
            "decompressor_lzma.c",
            "EINTEGRITY",
            "EOPNOTSUPP",
        ),
        "z_erofs_load_deflate_config": (
            "decompressor_deflate.c",
            "EINTEGRITY",
            "EOPNOTSUPP",
        ),
        "z_erofs_load_zstd_config": (
            "decompressor_zstd.c",
            "EINTEGRITY",
            "EOPNOTSUPP",
        ),
    }
    for function, (name, malformed, unsupported) in config_expectations.items():
        body = extract_function(source[name], function)
        if f"return ({malformed});" not in body:
            fail(f"{function} lacks malformed-config errno")
        if f"return ({unsupported});" not in body:
            fail(f"{function} lacks unsupported-config errno")
    if fixture["config"] != [
        {"class": "malformed", "errno": "EINTEGRITY"},
        {"class": "unsupported", "errno": "EOPNOTSUPP"},
    ]:
        fail("config fixture does not encode the B25 typed classes")

    lzma = extract_function(source["decompressor_lzma.c"], "z_erofs_lzma_decompress")
    require_once(lzma, "return (EOVERFLOW);", "LZMA ABI overflow")
    require_once(lzma, "return (ENOMEM);", "LZMA allocation failure")
    if not (
        lzma.index("xz_dec_microlzma_run")
        < lzma.index("xz_dec_microlzma_end")
        < lzma.index("return (error);")
    ):
        fail("LZMA state is not released before status publication")

    deflate = extract_function(
        source["decompressor_deflate.c"], "z_erofs_deflate_decompress"
    )
    require_once(deflate, "inflateEnd(&strm)", "Deflate cleanup")
    require_once(
        deflate,
        "if (error == 0 && endret != Z_OK)",
        "Deflate primary-error preservation",
    )
    if deflate.index("inflateEnd(&strm)") > deflate.index("return (error);"):
        fail("Deflate returns before releasing initialized state")

    zstd_source = source["decompressor_zstd.c"]
    enabled = extract_function(zstd_source, "z_erofs_zstd_decompress")
    require_once(enabled, "return (ENOMEM);", "Zstd allocation failure")
    require_once(
        enabled,
        "if (error == 0 && ZSTD_isError(ret))",
        "Zstd primary-error preservation",
    )
    require_once(enabled, "error = EOPNOTSUPP;", "Zstd parameter mapping")
    if enabled.count("error = EINTEGRITY;") != 4:
        fail("Zstd stream corruption/completion mappings changed")
    require_once(enabled, "error = EIO;", "Zstd release mapping")
    if "ZSTD_getErrorCode" in zstd_source:
        fail("B25 added a Zstd provider symbol outside the existing ABI")
    if zstd_source.count("return (EOPNOTSUPP);") < 3:
        fail("Zstd config/runtime disabled paths are not typed unsupported")
    expected_zstd = {
        "create_context": "ENOMEM",
        "set_window_limit": "EOPNOTSUPP",
        "decompress_stream": "EINTEGRITY",
        "free_context": "EIO",
    }
    actual_zstd = {
        case.get("operation"): case.get("expected") for case in fixture["zstd"]
    }
    if actual_zstd != expected_zstd or len(fixture["zstd"]) != len(expected_zstd):
        fail("Zstd phase fixture does not match the source contract")

    read_extent = extract_function(source["zdata.c"], "z_erofs_read_extent")
    decode_at = read_extent.index("error = z_erofs_decompress")
    meta_release_at = read_extent.index("erofs_put_metabuf(&buf)", decode_at)
    physical_release_at = read_extent.index("erofs_brelse(compressed)", decode_at)
    error_at = read_extent.index("if (error != 0)", decode_at)
    free_at = read_extent.index("free(decoded, M_EROFS)", error_at)
    if not (
        decode_at < meta_release_at < error_at < free_at
        and decode_at < physical_release_at < error_at < free_at
    ):
        fail("zdata input/output cleanup ordering changed")

    return {
        "callbacks": 2,
        "config_loaders": len(config_expectations),
        "dispatch_direct": True,
        "dispatch_fixture_cases": len(fixture["dispatch"]),
        "negative_errno": False,
        "zdata_unchanged": True,
    }


def compile_status_harness(
    source: dict[str, str], fixture: dict[str, object], artifacts: Path
) -> dict[str, int]:
    xz_helper = extract_function(source["decompressor_lzma.c"], "z_erofs_lzma_error")
    zlib_helper = extract_function(
        source["decompressor_deflate.c"], "z_erofs_deflate_error"
    )
    checks: list[str] = []
    for group, function in (
        ("xz", "z_erofs_lzma_error"),
        ("zlib", "z_erofs_deflate_error"),
    ):
        for case in fixture[group]:
            checks.append(
                f'\tcheck("{group}:{case["status"]}", '
                f'{function}({case["status"]}), {case["expected"]});'
            )
    program = f'''#include <stddef.h>
#include <stdio.h>

#define EIO 5
#define ENOMEM 12
#define EOVERFLOW 75
#define EOPNOTSUPP 95
#define EINTEGRITY 97

enum xz_ret {{
\tXZ_OK,
\tXZ_STREAM_END,
\tXZ_UNSUPPORTED_CHECK,
\tXZ_MEM_ERROR,
\tXZ_MEMLIMIT_ERROR,
\tXZ_FORMAT_ERROR,
\tXZ_OPTIONS_ERROR,
\tXZ_DATA_ERROR,
\tXZ_BUF_ERROR
}};

#define Z_OK 0
#define Z_STREAM_END 1
#define Z_NEED_DICT 2
#define Z_ERRNO (-1)
#define Z_STREAM_ERROR (-2)
#define Z_DATA_ERROR (-3)
#define Z_MEM_ERROR (-4)
#define Z_BUF_ERROR (-5)
#define Z_VERSION_ERROR (-6)

{xz_helper}

{zlib_helper}

static int failures;

static void
check(const char *name, int actual, int expected)
{{
\tif (actual != expected) {{
\t\tfprintf(stderr, "%s: %d != %d\\n", name, actual, expected);
\t\t++failures;
\t}}
}}

int
main(void)
{{
{chr(10).join(checks)}
\tif (failures != 0)
\t\treturn (1);
\tprintf("status-map cases={len(checks)}\\n");
\treturn (0);
}}
'''
    harness = artifacts / "B25-status-map.c"
    binary = artifacts / "B25-status-map"
    harness.write_text(program, encoding="ascii")
    compile_run = subprocess.run(
        ["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", str(harness), "-o", str(binary)],
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    (artifacts / "B25-status-map-build.stdout").write_text(
        compile_run.stdout, encoding="utf-8"
    )
    (artifacts / "B25-status-map-build.stderr").write_text(
        compile_run.stderr, encoding="utf-8"
    )
    if compile_run.returncode != 0:
        fail("B25 status-map harness did not compile")
    execute = subprocess.run(
        [str(binary)],
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    (artifacts / "B25-status-map.stdout").write_text(execute.stdout, encoding="utf-8")
    (artifacts / "B25-status-map.stderr").write_text(execute.stderr, encoding="utf-8")
    if execute.returncode != 0:
        fail("B25 status-map harness failed")
    binary.unlink()
    return {
        "xz": len(fixture["xz"]),
        "zlib": len(fixture["zlib"]),
        "zstd": len(fixture["zstd"]),
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--artifacts", required=True, type=Path)
    parser.add_argument("--baseline", required=True)
    parser.add_argument("--dut", required=True, type=Path)
    parser.add_argument("--fixture", required=True, type=Path)
    parser.add_argument("--root", required=True, type=Path)
    args = parser.parse_args()

    fixture = json.loads(args.fixture.read_text(encoding="ascii"))
    if fixture.get("schema") != 1 or fixture.get("candidate") != "P15-023":
        fail("invalid B25 fixture identity")
    for key in ("config", "dispatch", "xz", "zlib", "zstd"):
        if not isinstance(fixture.get(key), list) or not fixture[key]:
            fail(f"empty B25 fixture group: {key}")

    source_dir = args.dut / "src"
    current = {
        name: (source_dir / name).read_text(encoding="utf-8") for name in SOURCE_NAMES
    }
    base = {name: committed(args.root, args.baseline, name) for name in SOURCE_NAMES}
    verify_exact_small_transforms(current, base)
    contracts = verify_source_contracts(current, fixture)
    mappings = compile_status_harness(current, fixture, args.artifacts)

    report = {
        "candidate": "P15-023",
        "contracts": contracts,
        "mapping_cases": mappings,
        "status": "PASS",
    }
    (args.artifacts / "B25-codec-errors.json").write_text(
        json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
        encoding="ascii",
    )
    print(
        "B25 codec errno contract: "
        f"xz={mappings['xz']} zlib={mappings['zlib']} zstd={mappings['zstd']}"
    )


if __name__ == "__main__":
    main()
