#!/usr/bin/env python3
"""Compile, verify, and query a disposable Leo identity profile."""

from __future__ import annotations

import argparse
import json
import os
import shutil
import sys
import uuid
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

import yaml

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

LOCK_SCHEMA = "livingip.leoIdentityProfileLock.v1"
COMPILE_RECEIPT_SCHEMA = "livingip.leoIdentityCompileReceipt.v1"
QUERY_RECEIPT_SCHEMA = "livingip.leoIdentityQueryReceipt.v1"
SESSION_RECORD_SCHEMA = "livingip.leoTemporarySessionRecord.v1"
FIXED_QUERY = "What defines Leo's identity, and what is temporary?"
STATIC_PROFILE_ENTRIES = ("config.yaml", "skills", "plugins", "bin")
COMPILED_PROFILE_ENTRIES = frozenset(
    {
        "config.yaml",
        "skills",
        "plugins",
        "bin",
        "SOUL.md",
        "identity.json",
        "identity-manifest.json",
        "identity-lock.json",
        "compile-receipt.json",
        "sessions",
        "state",
    }
)
OMITTED_NONAUTHORITATIVE_ENTRIES = (
    "memories",
    "MEMORY.md",
    "USER.md",
    "platforms/pairing",
    ".skills_prompt_snapshot.json",
    ".hermes.md",
    "HERMES.md",
    "AGENTS.md",
    "CLAUDE.md",
    ".cursorrules",
)


class IdentityProfileError(identity.IdentityManifestError):
    """A compiled profile cannot be trusted or started."""


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


def _copy_static_profile(template: Path, target: Path) -> list[str]:
    _require(template.is_dir(), "profile template is missing")
    _require(not target.exists(), "compiled profile target already exists")
    target.mkdir(parents=True, mode=0o700)
    copied: list[str] = []
    for name in STATIC_PROFILE_ENTRIES:
        source = template / name
        if not source.exists():
            continue
        destination = target / name
        _require(not source.is_symlink(), f"unsafe symlinked static profile entry: {name}")
        if source.is_dir():
            _require(
                not any(path.is_symlink() for path in source.rglob("*")),
                f"unsafe symlink inside static profile entry: {name}",
            )
            shutil.copytree(source, destination, symlinks=False)
        elif source.is_file() and not source.is_symlink():
            if name == "config.yaml":
                try:
                    raw_config = yaml.safe_load(source.read_text(encoding="utf-8")) or {}
                except (OSError, TypeError, yaml.YAMLError) as exc:
                    raise IdentityProfileError(f"cannot sanitize profile config: {type(exc).__name__}") from exc
                _require(isinstance(raw_config, dict), "profile config must be a mapping")
                destination.write_text(
                    yaml.safe_dump(behavior.identity_config_projection(raw_config), sort_keys=False),
                    encoding="utf-8",
                )
                destination.chmod(0o600)
            else:
                shutil.copy2(source, destination)
        else:
            raise IdentityProfileError(f"unsafe static profile entry: {name}")
        copied.append(name)
    _require("config.yaml" in copied, "profile template config.yaml is missing")
    return copied


def _behavior(profile: Path, hermes_root: Path, deployment_root: Path) -> dict[str, Any]:
    return behavior.build_manifest(profile, hermes_root, deployment_root)


def _verify_runtime_binding(
    manifest: dict[str, Any],
    *,
    profile: Path,
    hermes_root: Path,
    deployment_root: Path,
) -> dict[str, Any]:
    observed = _behavior(profile, hermes_root, deployment_root)
    identity.validate_behavior_manifest(observed)
    expected = manifest["identity_inputs"]["runtime"]
    _require(
        observed["identity_runtime_sha256"] == expected["identity_runtime_sha256"], "identity runtime drift detected"
    )
    _require(observed["hermes_runtime"]["git_head"] == expected["hermes_git_head"], "Hermes Git drift detected")
    _require(
        observed["hermes_runtime"]["source_tree"]["sha256"] == expected["hermes_source_sha256"],
        "Hermes source drift detected",
    )
    _require(
        observed["teleo_infrastructure_runtime"]["git_head"] == expected["teleo_git_head"],
        "Teleo Git drift detected",
    )
    _require(
        observed["teleo_infrastructure_runtime"]["identity_source_tree"]["sha256"] == expected["teleo_source_sha256"],
        "Teleo identity source drift detected",
    )
    _require(observed["python_runtime"] == expected["python"], "Python runtime drift detected")
    _require(
        manifest["identity_inputs"]["source_commit"] == observed["teleo_infrastructure_runtime"]["git_head"],
        "source commit drift detected",
    )

    expected_model = manifest["identity_inputs"]["model"]
    observed_model = observed["model_runtime"]
    model_bindings = {
        "provider": observed_model.get("provider"),
        "default_model": observed_model.get("default_model"),
        "smart_routing": observed_model.get("smart_routing"),
        "smart_routing_model": observed_model.get("smart_routing_model"),
        "gateway_smart_model_routing": observed_model.get("gateway_smart_model_routing"),
        "model_section_sha256": observed_model.get("model_section_sha256"),
        "hosted_weights": observed_model.get("base_model_weights"),
        "actual_model_required_in_turn_receipt": observed_model.get(
            "actual_per_turn_model_must_be_recorded_by_the_call_receipt"
        ),
    }
    for field, observed_value in model_bindings.items():
        _require(expected_model.get(field) == observed_value, f"model runtime binding drift detected: {field}")

    expected_permissions = manifest["identity_inputs"]["permissions"]
    permission_bindings = {
        "identity_config_sha256": observed_model.get("identity_config_sha256"),
        "gateway_permissions_sha256": observed_model.get("gateway_permissions_sha256"),
        "tool_permissions_sha256": observed_model.get("tool_permissions_sha256"),
        "runtime_policy": observed_model.get("identity_runtime_policy"),
        "authorization_decision_required_in_runtime_receipt": True,
    }
    for field, observed_value in permission_bindings.items():
        _require(
            expected_permissions.get(field) == observed_value, f"permission runtime binding drift detected: {field}"
        )

    observed_skills = observed["components"]["procedural_skills"]["content"]
    expected_skills = manifest["identity_inputs"]["skills"]
    _require(expected_skills["content_sha256"] == observed_skills["sha256"], "skills runtime binding drift detected")
    _require(expected_skills["file_count"] == observed_skills["file_count"], "skills file count drift detected")
    return observed


def _view_file_sha256(profile: Path, name: str) -> str:
    path = profile / name
    _require(path.is_file() and not path.is_symlink(), f"compiled view is missing or unsafe: {name}")
    return behavior.file_sha256(path)


def _verify_nonauthoritative_surfaces(
    profile: Path,
    *,
    require_empty_sessions: bool,
    require_compile_receipt: bool,
) -> dict[str, Any]:
    omitted = {
        name: not (profile / name).exists() and not (profile / name).is_symlink()
        for name in OMITTED_NONAUTHORITATIVE_ENTRIES
    }
    _require(all(omitted.values()), "profile contains a forbidden nonauthoritative input surface")
    expected_entries = set(COMPILED_PROFILE_ENTRIES)
    if not require_compile_receipt:
        expected_entries.remove("compile-receipt.json")
    profile_entries = list(profile.iterdir())
    actual_entries = {path.name for path in profile_entries}
    _require(actual_entries == expected_entries, "compiled profile entry set is not exact")
    _require(not any(path.is_symlink() for path in profile_entries), "compiled profile entries must not be symlinks")
    if require_compile_receipt:
        compile_receipt = identity.load_json(profile / "compile-receipt.json", "identity compile receipt")
        _require(compile_receipt.get("schema") == COMPILE_RECEIPT_SCHEMA, "identity compile receipt schema is invalid")
        _require(compile_receipt.get("status") == "pass", "identity compile receipt status is invalid")
    state = profile / "state"
    sessions = profile / "sessions"
    for path, label in ((state, "state"), (sessions, "sessions")):
        _require(path.is_dir() and not path.is_symlink(), f"profile {label} directory is missing or unsafe")
    _require(not any(state.iterdir()), "profile state directory must remain empty")
    session_files = list(sessions.iterdir())
    if require_empty_sessions:
        _require(not session_files, "profile sessions must start empty")
    for path in session_files:
        _require(path.is_file() and not path.is_symlink() and path.suffix == ".json", "unsafe session entry detected")
        record = identity.load_json(path, "temporary session record")
        _require(
            set(record)
            == {
                "schema",
                "authority",
                "classification",
                "query_sha256",
                "answer_sha256",
                "may_be_used_as_evidence",
            },
            "temporary session record fields are invalid",
        )
        _require(record.get("schema") == SESSION_RECORD_SCHEMA, "temporary session record schema is invalid")
        _require(record.get("authority") == "none", "temporary session authority is invalid")
        _require(record.get("classification") == "temporary_noncanonical", "temporary session class is invalid")
        _require(record.get("may_be_used_as_evidence") is False, "temporary session cannot be evidence")
        _require(identity._is_sha256(record.get("query_sha256")), "temporary session query hash is invalid")
        _require(identity._is_sha256(record.get("answer_sha256")), "temporary session answer hash is invalid")
    return {
        "nonauthoritative_inputs_omitted": omitted,
        "session_file_count": len(session_files),
        "state_empty": True,
    }


def compile_profile(
    manifest: dict[str, Any],
    *,
    source_root: Path,
    profile_template: Path,
    profile: Path,
    hermes_root: Path,
    deployment_root: Path,
) -> dict[str, Any]:
    views = identity.verify_bound_sources(manifest, source_root)
    template_behavior = _behavior(profile_template, hermes_root, deployment_root)
    identity.validate_behavior_manifest(template_behavior)
    _require(
        template_behavior["identity_runtime_sha256"]
        == manifest["identity_inputs"]["runtime"]["identity_runtime_sha256"],
        "profile template does not match the pinned identity runtime",
    )
    copied = _copy_static_profile(profile_template, profile)
    try:
        for name, content in views.items():
            path = profile / name
            path.write_text(content, encoding="utf-8")
            path.chmod(0o600)
        identity.write_json(profile / "identity-manifest.json", manifest)
        (profile / "identity-manifest.json").chmod(0o600)
        for directory in (profile / "sessions", profile / "state"):
            directory.mkdir(mode=0o700)
        lock = {
            "schema": LOCK_SCHEMA,
            "manifest_sha256": manifest["manifest_sha256"],
            "identity_inputs_sha256": manifest["identity_inputs_sha256"],
            "compiled_views": {name: {"sha256": _view_file_sha256(profile, name)} for name in sorted(views)},
            "session_boundary": manifest["identity_inputs"]["session_boundary"],
        }
        identity.write_json(profile / "identity-lock.json", lock)
        (profile / "identity-lock.json").chmod(0o600)
        observed = _verify_runtime_binding(
            manifest,
            profile=profile,
            hermes_root=hermes_root,
            deployment_root=deployment_root,
        )
        for name, expected in manifest["compiled_views"].items():
            _require(_view_file_sha256(profile, name) == expected["sha256"], f"compiled {name} hash mismatch")
        soul_files = observed["components"]["runtime_identity"]["content"]["files"]
        soul = next((item for item in soul_files if item.get("path") == "SOUL.md"), None)
        _require(
            isinstance(soul, dict) and soul.get("sha256") == manifest["compiled_views"]["SOUL.md"]["sha256"],
            "SOUL runtime binding failed",
        )
        surface_readback = _verify_nonauthoritative_surfaces(
            profile,
            require_empty_sessions=True,
            require_compile_receipt=False,
        )
        receipt = {
            "schema": COMPILE_RECEIPT_SCHEMA,
            "status": "pass",
            "manifest_sha256": manifest["manifest_sha256"],
            "identity_inputs_sha256": manifest["identity_inputs_sha256"],
            "profile_static_entries": copied,
            "compiled_view_sha256": {
                name: manifest["compiled_views"][name]["sha256"] for name in sorted(manifest["compiled_views"])
            },
            "runtime_identity_sha256": observed["identity_runtime_sha256"],
            "session_directories_started_empty": all(
                not any((profile / name).iterdir()) for name in ("sessions", "state")
            ),
            "nonauthoritative_inputs_omitted": surface_readback["nonauthoritative_inputs_omitted"],
            "generated_views_are_authority": False,
        }
        identity.write_json(profile / "compile-receipt.json", receipt)
        return receipt
    except BaseException:
        shutil.rmtree(profile, ignore_errors=True)
        raise


def verify_profile(
    profile: Path,
    *,
    source_root: Path,
    hermes_root: Path,
    deployment_root: Path,
) -> tuple[dict[str, Any], dict[str, str], dict[str, Any]]:
    manifest = identity.load_json(profile / "identity-manifest.json", "compiled identity manifest")
    views = identity.verify_bound_sources(manifest, source_root)
    lock = identity.load_json(profile / "identity-lock.json", "identity profile lock")
    _verify_nonauthoritative_surfaces(
        profile,
        require_empty_sessions=False,
        require_compile_receipt=True,
    )
    _require(lock.get("schema") == LOCK_SCHEMA, "identity profile lock schema is invalid")
    _require(
        lock.get("manifest_sha256") == manifest["manifest_sha256"], "identity profile lock manifest drift detected"
    )
    _require(
        lock.get("identity_inputs_sha256") == manifest["identity_inputs_sha256"],
        "identity profile lock input drift detected",
    )
    compile_receipt = identity.load_json(profile / "compile-receipt.json", "identity compile receipt")
    _require(
        compile_receipt.get("manifest_sha256") == manifest["manifest_sha256"],
        "identity compile receipt manifest drift detected",
    )
    _require(
        compile_receipt.get("identity_inputs_sha256") == manifest["identity_inputs_sha256"],
        "identity compile receipt input drift detected",
    )
    for name, expected_content in views.items():
        expected_hash = behavior.sha256_bytes(expected_content.encode("utf-8"))
        _require(expected_hash == manifest["compiled_views"][name]["sha256"], f"manifest view drift detected: {name}")
        _require(_view_file_sha256(profile, name) == expected_hash, f"compiled view drift detected: {name}")
        _require(
            (lock.get("compiled_views") or {}).get(name, {}).get("sha256") == expected_hash,
            f"profile lock view drift detected: {name}",
        )
    observed = _verify_runtime_binding(
        manifest,
        profile=profile,
        hermes_root=hermes_root,
        deployment_root=deployment_root,
    )
    return manifest, views, observed


def query_profile(
    profile: Path,
    *,
    query: str,
    source_root: Path,
    hermes_root: Path,
    deployment_root: Path,
) -> dict[str, Any]:
    _require(query == FIXED_QUERY, "only the pinned identity readback query is allowed")
    manifest, _views, observed = verify_profile(
        profile,
        source_root=source_root,
        hermes_root=hermes_root,
        deployment_root=deployment_root,
    )
    structured = identity.load_json(profile / "identity.json", "compiled structured identity view")
    rule_count = len(structured["static_constitution"]["rules"])
    record_count = len(structured["database_identity"]["records"])
    fingerprint = structured["database_identity"]["database_fingerprint_sha256"]
    canonical_authority = structured["database_identity"]["canonical_authority_granted"]
    database_description = "canonical database" if canonical_authority else "synthetic noncanonical fixture"
    answer = (
        f"Leo is defined by {rule_count} pinned static constitutional rules and {record_count} active "
        f"database-derived identity rows from a {database_description} at fingerprint {fingerprint}. "
        "SOUL.md is a generated view; "
        "runtime/session memory is temporary, noncanonical, and cannot be evidence."
    )
    query_sha256 = behavior.sha256_bytes(query.encode("utf-8"))
    answer_sha256 = behavior.sha256_bytes(answer.encode("utf-8"))
    instance_id = uuid.uuid4().hex
    session_record = {
        "schema": SESSION_RECORD_SCHEMA,
        "authority": "none",
        "classification": "temporary_noncanonical",
        "query_sha256": query_sha256,
        "answer_sha256": answer_sha256,
        "may_be_used_as_evidence": False,
    }
    session_path = profile / "sessions" / f"{instance_id}.json"
    identity.write_json(session_path, session_record)
    receipt = {
        "schema": QUERY_RECEIPT_SCHEMA,
        "status": "pass",
        "started_at_utc": datetime.now(UTC).isoformat(),
        "process_id": os.getpid(),
        "runtime_instance_id": instance_id,
        "query": query,
        "query_sha256": query_sha256,
        "answer": answer,
        "answer_sha256": answer_sha256,
        "manifest_sha256": manifest["manifest_sha256"],
        "identity_inputs_sha256": manifest["identity_inputs_sha256"],
        "output_provenance": {
            "static_constitution_sha256": manifest["identity_inputs"]["identity_sources"]["static_constitution"][
                "semantic_sha256"
            ],
            "database_identity_sha256": manifest["identity_inputs"]["identity_sources"]["database_derived_identity"][
                "semantic_sha256"
            ],
            "database_fingerprint_sha256": fingerprint,
            "database_authority": structured["database_identity"]["authority"],
            "database_canonical_authority_granted": canonical_authority,
            "compiled_identity_sha256": manifest["compiled_views"]["identity.json"]["sha256"],
            "compiled_soul_sha256": manifest["compiled_views"]["SOUL.md"]["sha256"],
            "runtime_identity_sha256": observed["identity_runtime_sha256"],
            "actual_model_call_performed": False,
        },
        "authorization": {
            "declared_runtime_policy": manifest["identity_inputs"]["permissions"]["runtime_policy"],
            "authorization_decision_observed": False,
            "claim": "local_policy_binding_not_an_authorizer_readback",
        },
        "session": {
            "record_sha256": behavior.file_sha256(session_path),
            "classification": "temporary_noncanonical",
            "included_in_output_provenance": False,
            "may_be_used_as_evidence": False,
        },
    }
    return receipt


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)
    compile_command = subparsers.add_parser("compile")
    compile_command.add_argument("--manifest", type=Path, required=True)
    compile_command.add_argument("--source-root", type=Path, required=True)
    compile_command.add_argument("--profile-template", type=Path, required=True)
    compile_command.add_argument("--profile", type=Path, required=True)
    compile_command.add_argument("--hermes-root", type=Path, required=True)
    compile_command.add_argument("--deployment-root", type=Path, required=True)
    query_command = subparsers.add_parser("query")
    query_command.add_argument("--profile", type=Path, required=True)
    query_command.add_argument("--query", default=FIXED_QUERY)
    query_command.add_argument("--source-root", type=Path, required=True)
    query_command.add_argument("--hermes-root", type=Path, required=True)
    query_command.add_argument("--deployment-root", type=Path, required=True)
    args = parser.parse_args()
    try:
        if args.command == "compile":
            result = compile_profile(
                identity.load_json(args.manifest, "identity manifest"),
                source_root=args.source_root,
                profile_template=args.profile_template,
                profile=args.profile,
                hermes_root=args.hermes_root,
                deployment_root=args.deployment_root,
            )
        else:
            result = query_profile(
                args.profile,
                query=args.query,
                source_root=args.source_root,
                hermes_root=args.hermes_root,
                deployment_root=args.deployment_root,
            )
        print(json.dumps(result, sort_keys=True))
        return 0
    except identity.IdentityManifestError as exc:
        print(json.dumps({"status": "error", "error": "identity_drift", "message": str(exc)}))
        return 3


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