from __future__ import annotations

import argparse
import hashlib
import json
import re
from pathlib import Path


REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
ORIGINAL_STATIC = REPOSITORY_ROOT / "static"
FRONTEND_ROOT = REPOSITORY_ROOT / "app" / "frontend"
EVIDENCE_PATH = (
    REPOSITORY_ROOT
    / "docs"
    / "migration"
    / "evidence"
    / "slice-10"
    / "frontend-source-map.json"
)
EXPECTED_SOURCE_SHA256 = "c020d0bc68f8a96b352a0ffa62c913a0ea3f5ca0afc41f70969f833e447166a6"

RUNTIME_RANGES: dict[str, list[tuple[int, int]]] = {
    "pages/sentiment/page.js": [(1207, 1516)],
    "pages/pools/page.js": [(1517, 1915)],
    "pages/market/runtime.js": [(1916, 1957), (6893, 7719), (7969, 8426)],
    "pages/rotation/page.js": [(1958, 2124)],
    "pages/ladder/page.js": [(2125, 2216)],
    "pages/auction/page.js": [(2217, 2481)],
    "pages/themes/page.js": [(2482, 2583)],
    "pages/popularity/page.js": [(2584, 2659)],
    "pages/dragon-tiger/page.js": [(2660, 3028)],
    "pages/review/page.js": [(3029, 3470), (7720, 7968)],
    "pages/screener/page.js": [(3490, 4336), (6668, 6892)],
    "pages/mentor/page.js": [(4337, 4827)],
    "pages/heaven/page.js": [(4828, 6667)],
    "shared/export.js": [(8905, 9051)],
}


def digest(content: str) -> str:
    return hashlib.sha256(content.encode("utf-8")).hexdigest()


def marker(start: int, end: int, kind: str) -> str:
    return f"/* PRESERVATION-SOURCE-{kind} app.js:{start}-{end} */"


def wrapped_chunk(lines: list[str], start: int, end: int) -> str:
    content = "".join(lines[start - 1 : end])
    return f"{marker(start, end, 'BEGIN')}\n{content}{marker(start, end, 'END')}\n"


def complement_ranges(total: int, moved: set[int]) -> list[tuple[int, int]]:
    ranges: list[tuple[int, int]] = []
    start = 0
    for number in range(1, total + 1):
        if number in moved:
            if start:
                ranges.append((start, number - 1))
                start = 0
        elif not start:
            start = number
    if start:
        ranges.append((start, total))
    return ranges


def original_prefix(relative: str) -> str:
    source = ORIGINAL_STATIC / relative
    if not source.is_file():
        return ""
    return source.read_text(encoding="utf-8").rstrip("\n") + "\n\n"


def extract_written_chunks(paths: list[Path]) -> dict[tuple[int, int], str]:
    pattern = re.compile(
        r"/\* PRESERVATION-SOURCE-BEGIN app\.js:(\d+)-(\d+) \*/\n"
        r"(.*?)"
        r"/\* PRESERVATION-SOURCE-END app\.js:\1-\2 \*/\n?",
        re.DOTALL,
    )
    chunks: dict[tuple[int, int], str] = {}
    for path in paths:
        content = path.read_text(encoding="utf-8")
        for match in pattern.finditer(content):
            key = (int(match.group(1)), int(match.group(2)))
            if key in chunks:
                raise RuntimeError(f"Duplicate preserved range {key}")
            chunks[key] = match.group(3)
    return chunks


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Reproduce the one-time Slice 10 frontend source split"
    )
    parser.add_argument(
        "--apply",
        action="store_true",
        help="perform the historical split; this rewrites frontend runtime files",
    )
    args = parser.parse_args()
    if not args.apply:
        parser.error(
            "this migration-only tool rewrites files; pass --apply only when "
            "reproducing Slice 10 from its documented pre-split checkpoint"
        )
    source_path = ORIGINAL_STATIC / "app.js"
    target_path = FRONTEND_ROOT / "app.js"
    source_bytes = source_path.read_bytes()
    source_sha256 = hashlib.sha256(source_bytes).hexdigest()
    if source_sha256 != EXPECTED_SOURCE_SHA256:
        raise RuntimeError(
            f"Original app.js changed: expected {EXPECTED_SOURCE_SHA256}, got {source_sha256}"
        )
    if not target_path.is_file():
        raise RuntimeError(f"Missing target runtime: {target_path}")
    if hashlib.sha256(target_path.read_bytes()).hexdigest() != EXPECTED_SOURCE_SHA256:
        raise RuntimeError("Target app.js is not the exact pre-split original")

    source = source_bytes.decode("utf-8")
    lines = source.splitlines(keepends=True)
    moved_lines: set[int] = set()
    for ranges in RUNTIME_RANGES.values():
        for start, end in ranges:
            overlap = moved_lines.intersection(range(start, end + 1))
            if overlap:
                raise RuntimeError(f"Overlapping source ranges at line {min(overlap)}")
            moved_lines.update(range(start, end + 1))

    core_ranges = complement_ranges(len(lines), moved_lines)
    target_path.write_text(
        "\n".join(wrapped_chunk(lines, start, end).rstrip("\n") for start, end in core_ranges)
        + "\n",
        encoding="utf-8",
    )

    written_paths = [target_path]
    for relative, ranges in RUNTIME_RANGES.items():
        path = FRONTEND_ROOT / relative
        path.parent.mkdir(parents=True, exist_ok=True)
        chunks = "\n".join(
            wrapped_chunk(lines, start, end).rstrip("\n") for start, end in ranges
        )
        path.write_text(original_prefix(relative) + chunks + "\n", encoding="utf-8")
        written_paths.append(path)

    extracted = extract_written_chunks(written_paths)
    expected_ranges = {
        source_range for ranges in RUNTIME_RANGES.values() for source_range in ranges
    } | set(core_ranges)
    if set(extracted) != expected_ranges:
        raise RuntimeError("Written source ranges do not cover the original runtime exactly")

    reassembled = [""] * len(lines)
    for (start, end), content in extracted.items():
        chunk_lines = content.splitlines(keepends=True)
        if len(chunk_lines) != end - start + 1:
            raise RuntimeError(f"Line count changed in preserved range {start}-{end}")
        reassembled[start - 1 : end] = chunk_lines
    reassembled_source = "".join(reassembled)
    if reassembled_source != source:
        raise RuntimeError("Split runtime cannot be reassembled byte-for-byte")

    manifest = {
        "source": "static/app.js",
        "source_sha256": source_sha256,
        "source_line_count": len(lines),
        "reassembled_sha256": digest(reassembled_source),
        "all_source_lines_preserved": True,
        "core": {
            "target": "app/frontend/app.js",
            "ranges": [{"start": start, "end": end} for start, end in core_ranges],
        },
        "modules": [
            {
                "target": f"app/frontend/{relative}",
                "ranges": [
                    {
                        "start": start,
                        "end": end,
                        "sha256": digest("".join(lines[start - 1 : end])),
                    }
                    for start, end in ranges
                ],
            }
            for relative, ranges in RUNTIME_RANGES.items()
        ],
    }
    EVIDENCE_PATH.parent.mkdir(parents=True, exist_ok=True)
    EVIDENCE_PATH.write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
    )
    print(json.dumps(manifest, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
