#!/usr/bin/env python3
"""Generate and independently rebuild Leo's deterministic identity version manifest.

The version manifest is an operator-facing projection over the stricter identity
compiler.  It names the runtime, Git commit, individual skills, database
version, compiled identity, and session boundary without turning generated
views or session history into provenance.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path, PurePosixPath
from typing import Any

if __package__ in {None, ""}:
    sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from scripts import leo_behavior_manifest as behavior
from scripts import leo_identity_manifest as identity

SCHEMA = "livingip.leoIdentityVersionManifest.v1"
DEFAULT_PROFILE = Path("fixtures/working-leo/leo-identity-v1/profile-template")
DEFAULT_HERMES_ROOT = Path("fixtures/working-leo/leo-identity-v1/hermes-runtime")
DEFAULT_DEPLOYMENT_ROOT = Path(".")
DEFAULT_SOURCE_ROOT = Path(".")
DEFAULT_DATABASE_FINGERPRINT = Path("fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json")
DEFAULT_CONSTITUTION = Path("fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json")
DEFAULT_DATABASE_IDENTITY = Path("fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json")
HEX_64 = re.compile(r"^[0-9a-f]{64}$")


class IdentityVersionManifestError(identity.IdentityManifestError):
    """The identity version receipt is incomplete, inconsistent, or drifted."""


def _require(condition: bool, message: str) -> None:
    if not condition:
        raise IdentityVersionManifestError(message)


def expected_authority_contract() -> dict[str, str]:
    return {
        "durable_knowledge_authority": "approved_postgres_rows_with_bound_evidence",
        "compiled_identity_role": "derived_view_from_versioned_inputs_not_authority",
        "session_history_role": "temporary_or_persisted_context_not_provenance",
    }


def _runtime_version(identity_manifest: dict[str, Any]) -> dict[str, Any]:
    core = identity_manifest["identity_inputs"]
    runtime = core["runtime"]
    model = core["model"]
    return {
        "source_commit": core["source_commit"],
        "identity_runtime_sha256": runtime["identity_runtime_sha256"],
        "teleo_infrastructure": {
            "git_head": runtime["teleo_git_head"],
            "identity_source_sha256": runtime["teleo_source_sha256"],
        },
        "hermes": {
            "git_head": runtime["hermes_git_head"],
            "source_sha256": runtime["hermes_source_sha256"],
        },
        "python": runtime["python"],
        "model": {
            "provider": model["provider"],
            "default_model": model["default_model"],
            "model_section_sha256": model["model_section_sha256"],
            "hosted_weights": model["hosted_weights"],
            "actual_model_required_in_turn_receipt": model["actual_model_required_in_turn_receipt"],
        },
    }


def _canonical_database_version(identity_manifest: dict[str, Any]) -> dict[str, Any]:
    core = identity_manifest["identity_inputs"]
    database = core["canonical_database"]
    source = core["identity_sources"]["database_derived_identity"]
    provenance = source["provenance"]
    return {
        "authority": database["authority"],
        "canonical_authority_granted": database["canonical_authority_granted"],
        "database": database["database"],
        "database_user": database["database_user"],
        "system_identifier": database["system_identifier"],
        "fingerprint_sha256": database["fingerprint_sha256"],
        "table_rows_sha256": database["table_rows_sha256"],
        "structure_sha256": database["structure_sha256"],
        "table_count": database["table_count"],
        "total_rows": database["total_rows"],
        "identity_records_sha256": source["semantic_sha256"],
        "identity_record_count": source["record_count"],
        "source_mode": provenance["mode"],
        "same_transaction_as_fingerprint": provenance["same_transaction_as_fingerprint"],
        "export_receipt_sha256": provenance["export_receipt_sha256"],
    }


def _compiled_identity_version(identity_manifest: dict[str, Any]) -> dict[str, Any]:
    return {
        "role": "derived_view_from_versioned_inputs_not_authority",
        "identity_inputs_sha256": identity_manifest["identity_inputs_sha256"],
        "identity_manifest_sha256": identity_manifest["manifest_sha256"],
        "views": identity_manifest["compiled_views"],
    }


def _skill_versions(behavior_manifest: dict[str, Any], identity_manifest: dict[str, Any]) -> dict[str, Any]:
    content = behavior_manifest["components"]["procedural_skills"]["content"]
    identity.validate_content_manifest(content, label="procedural skills")
    groups: dict[str, list[dict[str, Any]]] = {}
    for item in content["files"]:
        parts = PurePosixPath(item["path"]).parts
        _require(
            len(parts) >= 3 and parts[0] == "skills" and bool(parts[1]),
            "procedural skill file is not inside a named skill directory",
        )
        relative_path = PurePosixPath(*parts[2:]).as_posix()
        groups.setdefault(parts[1], []).append(
            {
                "path": relative_path,
                "bytes": item["bytes"],
                "mode": item["mode"],
                "sha256": item["sha256"],
            }
        )
    _require(bool(groups), "at least one named skill is required")
    skills = []
    for name in sorted(groups):
        files = sorted(groups[name], key=lambda item: item["path"])
        skills.append(
            {
                "name": name,
                "files": files,
                "file_count": len(files),
                "total_bytes": sum(item["bytes"] for item in files),
                "content_sha256": behavior.canonical_sha256(files),
            }
        )
    result = {
        "content_sha256": content["sha256"],
        "file_count": content["file_count"],
        "total_bytes": content["total_bytes"],
        "skills": skills,
    }
    expected = identity_manifest["identity_inputs"]["skills"]
    _require(result["content_sha256"] == expected["content_sha256"], "skill aggregate hash drift detected")
    _require(result["file_count"] == expected["file_count"], "skill aggregate file count drift detected")
    return result


def _validate_skill_versions(value: Any, identity_manifest: dict[str, Any]) -> None:
    _require(isinstance(value, dict), "skill versions are missing")
    _require(
        set(value) == {"content_sha256", "file_count", "total_bytes", "skills"},
        "skill version fields are invalid",
    )
    _require(isinstance(value.get("skills"), list) and bool(value["skills"]), "named skills are missing")
    names: list[str] = []
    reconstructed_files: list[dict[str, Any]] = []
    total_bytes = 0
    for skill in value["skills"]:
        _require(
            isinstance(skill, dict) and set(skill) == {"name", "files", "file_count", "total_bytes", "content_sha256"},
            "named skill fields are invalid",
        )
        name = skill.get("name")
        files = skill.get("files")
        _require(
            isinstance(name, str) and bool(name) and PurePosixPath(name).parts == (name,) and name not in {".", ".."},
            "skill name is invalid",
        )
        _require(isinstance(files, list) and bool(files), f"skill {name} has no files")
        names.append(name)
        paths: list[str] = []
        skill_bytes = 0
        for item in files:
            _require(
                isinstance(item, dict) and set(item) == {"path", "bytes", "mode", "sha256"},
                f"skill {name} file fields are invalid",
            )
            path = item.get("path")
            size = item.get("bytes")
            mode = item.get("mode")
            _require(
                isinstance(path, str)
                and bool(path)
                and not path.startswith("/")
                and ".." not in PurePosixPath(path).parts,
                f"skill {name} path is invalid",
            )
            _require(isinstance(size, int) and not isinstance(size, bool) and size >= 0, "skill byte size is invalid")
            _require(isinstance(mode, str) and bool(re.fullmatch(r"0o[0-7]{3}", mode)), "skill mode is invalid")
            _require(
                isinstance(item.get("sha256"), str) and bool(HEX_64.fullmatch(item["sha256"])), "skill hash is invalid"
            )
            paths.append(path)
            skill_bytes += size
            reconstructed_files.append({**item, "path": f"skills/{name}/{path}"})
        _require(paths == sorted(paths) and len(paths) == len(set(paths)), f"skill {name} paths are not sorted")
        _require(skill.get("file_count") == len(files), f"skill {name} file count is invalid")
        _require(skill.get("total_bytes") == skill_bytes, f"skill {name} byte count is invalid")
        _require(skill.get("content_sha256") == behavior.canonical_sha256(files), f"skill {name} hash drift detected")
        total_bytes += skill_bytes
    _require(names == sorted(names) and len(names) == len(set(names)), "skill names are not unique and sorted")
    reconstructed_files.sort(key=lambda item: item["path"])
    aggregate = behavior.canonical_sha256({"files": reconstructed_files, "missing": [], "symlinks": []})
    _require(value.get("content_sha256") == aggregate, "skill aggregate content drift detected")
    _require(value.get("file_count") == len(reconstructed_files), "skill aggregate file count is invalid")
    _require(value.get("total_bytes") == total_bytes, "skill aggregate byte count is invalid")
    expected = identity_manifest["identity_inputs"]["skills"]
    _require(value["content_sha256"] == expected["content_sha256"], "skills are not bound to the identity manifest")
    _require(value["file_count"] == expected["file_count"], "skill count is not bound to the identity manifest")


def assemble_version_manifest(
    *, behavior_manifest: dict[str, Any], identity_manifest: dict[str, Any]
) -> dict[str, Any]:
    identity.validate_behavior_manifest(behavior_manifest)
    identity.validate_identity_manifest(identity_manifest)
    core = identity_manifest["identity_inputs"]
    _require(
        behavior_manifest["identity_runtime_sha256"] == core["runtime"]["identity_runtime_sha256"],
        "behavior runtime is not bound to the identity manifest",
    )
    _require(
        behavior_manifest["teleo_infrastructure_runtime"]["git_head"] == core["source_commit"],
        "behavior source commit is not bound to the identity manifest",
    )
    stable = {
        "schema": SCHEMA,
        "identity_name": "Leo",
        "runtime_version": _runtime_version(identity_manifest),
        "skill_versions": _skill_versions(behavior_manifest, identity_manifest),
        "canonical_database_version": _canonical_database_version(identity_manifest),
        "compiled_identity_version": _compiled_identity_version(identity_manifest),
        "session_boundary": core["session_boundary"],
        "authority_contract": expected_authority_contract(),
        "identity_manifest": identity_manifest,
    }
    return {**stable, "manifest_sha256": behavior.canonical_sha256(stable)}


def build_version_manifest(
    *,
    profile: Path,
    hermes_root: Path,
    deployment_root: Path,
    source_root: Path,
    database_fingerprint: Path,
    constitution: Path,
    database_identity: Path,
) -> dict[str, Any]:
    behavior_manifest = behavior.build_manifest(profile, hermes_root, deployment_root)
    identity_manifest = identity.build_identity_manifest(
        behavior_manifest=behavior_manifest,
        database_fingerprint_path=database_fingerprint,
        constitution_path=constitution,
        database_identity_path=database_identity,
        source_root=source_root,
    )
    identity.verify_bound_sources(identity_manifest, source_root)
    return assemble_version_manifest(
        behavior_manifest=behavior_manifest,
        identity_manifest=identity_manifest,
    )


def validate_version_manifest(value: dict[str, Any]) -> None:
    _require(
        set(value)
        == {
            "schema",
            "identity_name",
            "runtime_version",
            "skill_versions",
            "canonical_database_version",
            "compiled_identity_version",
            "session_boundary",
            "authority_contract",
            "identity_manifest",
            "manifest_sha256",
        },
        "identity version manifest fields are invalid",
    )
    _require(value.get("schema") == SCHEMA, "unsupported identity version manifest schema")
    _require(value.get("identity_name") == "Leo", "identity version must describe Leo")
    expected_hash = value.get("manifest_sha256")
    _require(isinstance(expected_hash, str) and bool(HEX_64.fullmatch(expected_hash)), "manifest hash is invalid")
    stable = {key: item for key, item in value.items() if key != "manifest_sha256"}
    _require(behavior.canonical_sha256(stable) == expected_hash, "identity version manifest hash drift detected")
    embedded = value.get("identity_manifest")
    _require(isinstance(embedded, dict), "embedded identity manifest is missing")
    identity.validate_identity_manifest(embedded)
    core = embedded["identity_inputs"]
    _require(value.get("runtime_version") == _runtime_version(embedded), "runtime version projection drift detected")
    _validate_skill_versions(value.get("skill_versions"), embedded)
    _require(
        value.get("canonical_database_version") == _canonical_database_version(embedded),
        "canonical database version projection drift detected",
    )
    _require(
        value.get("compiled_identity_version") == _compiled_identity_version(embedded),
        "compiled identity version projection drift detected",
    )
    _require(value.get("session_boundary") == core["session_boundary"], "session boundary projection drift detected")
    _require(
        value.get("authority_contract") == expected_authority_contract(), "identity authority contract drift detected"
    )


def verify_against_inputs(
    value: dict[str, Any],
    *,
    profile: Path,
    hermes_root: Path,
    deployment_root: Path,
    source_root: Path,
    database_fingerprint: Path,
    constitution: Path,
    database_identity: Path,
) -> None:
    validate_version_manifest(value)
    rebuilt = build_version_manifest(
        profile=profile,
        hermes_root=hermes_root,
        deployment_root=deployment_root,
        source_root=source_root,
        database_fingerprint=database_fingerprint,
        constitution=constitution,
        database_identity=database_identity,
    )
    _require(rebuilt == value, "identity version manifest does not match the supplied versioned inputs")


def write_json(path: Path, value: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")


def summary(value: dict[str, Any]) -> dict[str, Any]:
    return {
        "status": "ok",
        "manifest_sha256": value["manifest_sha256"],
        "source_commit": value["runtime_version"]["source_commit"],
        "skills": [item["name"] for item in value["skill_versions"]["skills"]],
        "canonical_database": {
            "authority": value["canonical_database_version"]["authority"],
            "canonical_authority_granted": value["canonical_database_version"]["canonical_authority_granted"],
            "fingerprint_sha256": value["canonical_database_version"]["fingerprint_sha256"],
        },
        "compiled_identity_sha256": value["compiled_identity_version"]["identity_inputs_sha256"],
        "session_boundary": value["session_boundary"]["classification"],
    }


def _add_input_arguments(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--profile", type=Path, default=DEFAULT_PROFILE)
    parser.add_argument("--hermes-root", type=Path, default=DEFAULT_HERMES_ROOT)
    parser.add_argument("--deployment-root", type=Path, default=DEFAULT_DEPLOYMENT_ROOT)
    parser.add_argument("--source-root", type=Path, default=DEFAULT_SOURCE_ROOT)
    parser.add_argument("--database-fingerprint", type=Path, default=DEFAULT_DATABASE_FINGERPRINT)
    parser.add_argument("--constitution", type=Path, default=DEFAULT_CONSTITUTION)
    parser.add_argument("--database-identity", type=Path, default=DEFAULT_DATABASE_IDENTITY)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)
    generate = subparsers.add_parser("generate", help="generate a deterministic identity version manifest")
    _add_input_arguments(generate)
    generate.add_argument("--output", type=Path, required=True)
    verify = subparsers.add_parser("verify", help="rebuild and compare a manifest from its versioned inputs")
    verify.add_argument("--manifest", type=Path, required=True)
    _add_input_arguments(verify)
    args = parser.parse_args()
    try:
        if args.command == "generate":
            manifest = build_version_manifest(
                profile=args.profile,
                hermes_root=args.hermes_root,
                deployment_root=args.deployment_root,
                source_root=args.source_root,
                database_fingerprint=args.database_fingerprint,
                constitution=args.constitution,
                database_identity=args.database_identity,
            )
            write_json(args.output, manifest)
        else:
            manifest = identity.load_json(args.manifest, "identity version manifest")
            verify_against_inputs(
                manifest,
                profile=args.profile,
                hermes_root=args.hermes_root,
                deployment_root=args.deployment_root,
                source_root=args.source_root,
                database_fingerprint=args.database_fingerprint,
                constitution=args.constitution,
                database_identity=args.database_identity,
            )
        print(json.dumps(summary(manifest), sort_keys=True))
        return 0
    except (identity.IdentityManifestError, OSError, ValueError) as exc:
        print(
            json.dumps(
                {
                    "status": "error",
                    "error": "identity_version_manifest_invalid",
                    "message": str(exc),
                },
                sort_keys=True,
            )
        )
        return 2


if __name__ == "__main__":
    raise SystemExit(main())
