#!/usr/bin/env python3
"""Build the redacted, owner-only v1 successor projection from Task25 evidence."""

from __future__ import annotations

import hashlib
import json
import os
import re
import stat
import tempfile
from pathlib import Path
from typing import Any

ARCHIVE = Path(
    "/home/cube/.hermes/profiles/dualcoachtest/data/rehearsal-reset-archives/"
    "2009ac177177839cefddb98f285e27fa"
)
OUTPUT = Path(
    "/home/cube/projects/richard/traning coach/.omo/evidence/task26/"
    "task26-v1-successor-archive"
)
EXPECTED = {
    "manifest": "ca4811444e114a06749620b9a44e05c8bd62bc3223ec80b1b5740c832c4a84c1",
    "receipt": "e81a84a8584400401f83aace36283dee18660144ede11e854758e725796e7fb3",
    "drafts": "509ba81dc6356134f61d31d33f664ca0531391c62abe7f785aeb51122a2fe4a1",
}
FORBIDDEN = re.compile(
    r"(?i)(^|[^a-z0-9])trainer([^a-z0-9]|$)|"
    r"trainer_[a-z0-9_]*|[a-z0-9_]*_trainer[a-z0-9_]*|"
    r"trainer-review|trainer_review|trainer-session|trainer_session|"
    r"claim_trainer|pilot_trainer|trb[0-9]+|pt1:|트레이너|오늘PT기록|PT기록"
)


def canonical(value: object) -> bytes:
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    ).encode("utf-8")


def sha256(value: bytes) -> str:
    return hashlib.sha256(value).hexdigest()


def read_regular(path: Path, label: str) -> bytes:
    info = path.stat(follow_symlinks=False)
    if path.is_symlink() or not stat.S_ISREG(info.st_mode):
        raise RuntimeError(f"{label} is not a regular file")
    return path.read_bytes()


def atomic_write(path: Path, value: object) -> bytes:
    raw = canonical(value)
    path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
    path.parent.chmod(0o700)
    descriptor, temporary = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.")
    try:
        with os.fdopen(descriptor, "wb") as handle:
            handle.write(raw)
            handle.flush()
            os.fsync(handle.fileno())
        os.chmod(temporary, 0o600)
        os.replace(temporary, path)
        path.chmod(0o600)
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)
    return raw


def draft_projection(raw: bytes) -> tuple[dict[str, object], list[dict[str, str]]]:
    source = json.loads(raw)
    if not isinstance(source, dict) or len(source) != 3:
        raise RuntimeError("source draft ledger is not the expected three-record archive")
    projected: dict[str, object] = {}
    mapping: list[dict[str, str]] = []
    for record_id in sorted(source):
        record = source[record_id]
        if not isinstance(record_id, str) or not isinstance(record, dict):
            raise RuntimeError("source draft ledger is malformed")
        review = record.get("coach_review")
        if not isinstance(review, dict):
            raise RuntimeError("source review projection is malformed")
        expected_keys = {
            "schema_version",
            "current_targets",
            "proposed_targets",
            "decision",
            "confidence",
            "evidence_ids",
            "next_checkin_focus_ids",
            "interpretation",
            "warnings",
            "facts",
            "trainer_handoff",
            "revision_binding_digest",
        }
        if set(review) != expected_keys or review.get("schema_version") != "nutrition-coach-review-v2":
            raise RuntimeError("source review projection is not the expected historical schema")
        successor = dict(record)
        successor_review = {
            key: value for key, value in review.items() if key != "trainer_handoff"
        }
        successor_review["schema_version"] = "nutrition-coach-review-v3"
        successor_review["owner_review_notes"] = []
        successor["coach_review"] = successor_review
        projected[record_id] = successor
        mapping.append(
            {
                "source_record_sha256": sha256(canonical(record)),
                "destination_record_sha256": sha256(canonical(successor)),
            }
        )
    if len({row["source_record_sha256"] for row in mapping}) != len(mapping):
        raise RuntimeError("source record identities are not unique")
    return projected, mapping


def source_hash(name: str, path: Path) -> str:
    value = sha256(read_regular(path, name))
    if value != EXPECTED[name]:
        raise RuntimeError(f"{name} digest is not the immutable source pin")
    return value


def ensure_private_tree(root: Path) -> None:
    for path in sorted(root.rglob("*")):
        info = path.lstat()
        if path.is_symlink():
            raise RuntimeError("successor projection contains a symlink")
        if path.is_dir():
            if stat.S_IMODE(info.st_mode) != 0o700:
                raise RuntimeError("successor projection directory mode is invalid")
        elif stat.S_ISREG(info.st_mode):
            if stat.S_IMODE(info.st_mode) != 0o600:
                raise RuntimeError("successor projection file mode is invalid")
        else:
            raise RuntimeError("successor projection contains an unsafe entry")


def main() -> None:
    manifest_sha = source_hash("manifest", ARCHIVE / "manifest.json")
    receipt_sha = source_hash("receipt", ARCHIVE / "receipt.json")
    drafts_sha = source_hash(
        "drafts", ARCHIVE / "payload" / "data" / "owner-actions" / "drafts.json"
    )
    projected, record_map = draft_projection(
        read_regular(
            ARCHIVE / "payload" / "data" / "owner-actions" / "drafts.json",
            "drafts",
        )
    )
    active_root = OUTPUT / "active-v1"
    drafts_path = active_root / "data" / "owner-actions" / "drafts.json"
    draft_bytes = atomic_write(drafts_path, projected)
    rendered = draft_bytes.decode("utf-8")
    matches = FORBIDDEN.findall(rendered)
    if matches:
        raise RuntimeError("successor active projection contains a forbidden value")
    receipt = {
        "schema": "task26-successor-projection-migration-v1",
        "source_archive": {
            "id": ARCHIVE.name,
            "manifest_sha256": manifest_sha,
            "receipt_sha256": receipt_sha,
            "draft_ledger_sha256": drafts_sha,
        },
        "destination": {
            "record_count": len(projected),
            "draft_ledger_sha256": sha256(draft_bytes),
            "forbidden_key_or_value_count": 0,
        },
        "record_hash_map": sorted(record_map, key=lambda row: row["source_record_sha256"]),
    }
    receipt_path = OUTPUT / "migration-receipt.redacted.json"
    receipt_bytes = atomic_write(receipt_path, receipt)
    entries = []
    for path in sorted(active_root.rglob("*")):
        if path.is_dir():
            continue
        raw = read_regular(path, "successor active projection")
        entries.append(
            {
                "path": str(path.relative_to(OUTPUT)),
                "type": "regular_file",
                "mode": f"{stat.S_IMODE(path.stat().st_mode):04o}",
                "size": len(raw),
                "sha256": sha256(raw),
                "symlink_target": None,
            }
        )
    manifest = {
        "schema": "task26-successor-projection-manifest-v1",
        "source_archive": receipt["source_archive"],
        "migration_receipt_path": str(receipt_path.relative_to(OUTPUT)),
        "migration_receipt_sha256": sha256(receipt_bytes),
        "entries": entries,
        "active_projection_sha256": sha256(canonical(entries)),
    }
    atomic_write(OUTPUT / "manifest.json", manifest)
    for directory in (OUTPUT, active_root, active_root / "data", active_root / "data" / "owner-actions"):
        directory.chmod(0o700)
    ensure_private_tree(OUTPUT)


if __name__ == "__main__":
    main()
