from __future__ import annotations

import hashlib
import json
from datetime import datetime, timezone
from typing import Any

from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from api.models.review_contract import ReviewContract, ReviewReceipt
from api.schemas.review_contract import (
    GateReviewState,
    OwnerReviewState,
    ReviewAggregateRead,
)


def canonical_json(value: Any) -> bytes:
    return json.dumps(
        value, sort_keys=True, separators=(",", ":"), ensure_ascii=True
    ).encode()


def digest_document(value: Any) -> str:
    return hashlib.sha256(canonical_json(value)).hexdigest()


def _assertion_id(text: str) -> str:
    return "assertion:" + hashlib.sha256(text.encode()).hexdigest()[:20]


def normalize_contract(raw: dict[str, Any]) -> tuple[dict[str, Any], str]:
    """Validate v1 or adapt the WP-0024 prototype without executing its checks."""
    digest = digest_document(raw)
    if raw.get("interface") == "railiance.owner-review" and raw.get("version") == 1:
        owners_raw = raw.get("owners")
        task_owners = raw.get("task_owners")
        hashes = raw.get("artifact_sha256")
        if (
            not isinstance(owners_raw, dict)
            or not owners_raw
            or not isinstance(task_owners, dict)
            or not task_owners
        ):
            raise HTTPException(
                422, "legacy contract requires non-empty owners and task_owners"
            )
        if not isinstance(hashes, dict):
            raise HTTPException(422, "legacy contract requires artifact_sha256")
        owners = []
        for owner_id, scope in owners_raw.items():
            artifacts = scope.get("artifacts") or []
            assertions = scope.get("assertions") or []
            checks = scope.get("checks") or []
            if (
                not artifacts
                or not assertions
                or not checks
                or any(path not in hashes for path in artifacts)
            ):
                raise HTTPException(422, f"{owner_id}: incomplete legacy owner scope")
            owners.append(
                {
                    "id": owner_id,
                    "artifact_ids": artifacts,
                    "assertions": [
                        {"id": _assertion_id(text), "statement": text}
                        for text in assertions
                    ],
                    "check_ids": checks,
                }
            )
        owner_ids = {item["id"] for item in owners}
        gates = []
        for gate_id, required in task_owners.items():
            if (
                not required
                or len(required) != len(set(required))
                or not set(required) <= owner_ids
            ):
                raise HTTPException(422, f"{gate_id}: invalid legacy owner set")
            gates.append({"id": gate_id, "policy": "all_required", "owners": required})
        normalized = {
            "schema_version": "review-contract/v1",
            "contract_key": raw.get("workplan_id"),
            "subject": {"kind": "workplan", "id": raw.get("workplan_id")},
            "owners": owners,
            "gates": gates,
            "artifacts": {
                path: {"algorithm": "sha256", "digest": value}
                for path, value in hashes.items()
            },
            "allowed_dispositions": [
                item.replace("-", "_") for item in (raw.get("decisions") or [])
            ],
            "legacy": {
                "interface": raw["interface"],
                "version": 1,
                "source_digest": digest,
            },
        }
        return validate_v1(normalized), digest
    if raw.get("schema_version") != "review-contract/v1":
        raise HTTPException(422, "schema_version must be review-contract/v1")
    return validate_v1(raw), digest


def validate_v1(doc: dict[str, Any]) -> dict[str, Any]:
    contract_key, subject = doc.get("contract_key"), doc.get("subject")
    owners, gates, artifacts = doc.get("owners"), doc.get("gates"), doc.get("artifacts")
    dispositions = doc.get("allowed_dispositions")
    if not isinstance(contract_key, str) or not contract_key:
        raise HTTPException(422, "contract_key is required")
    if (
        not isinstance(subject, dict)
        or not subject.get("kind")
        or not subject.get("id")
    ):
        raise HTTPException(422, "typed subject kind and id are required")
    if (
        not isinstance(owners, list)
        or not owners
        or not isinstance(gates, list)
        or not gates
    ):
        raise HTTPException(422, "non-empty owners and gates are required")
    if not isinstance(artifacts, dict) or not artifacts:
        raise HTTPException(422, "revision-pinned artifacts are required")
    if (
        set(dispositions or []) != {"approve", "request_changes"}
        or len(dispositions) != 2
    ):
        raise HTTPException(
            422, "allowed_dispositions must contain approve and request_changes only"
        )
    owner_ids: set[str] = set()
    for owner in owners:
        if not isinstance(owner, dict):
            raise HTTPException(422, "owner entries must be objects")
        owner_id = owner.get("id")
        artifact_ids = owner.get("artifact_ids") or []
        assertions = owner.get("assertions") or []
        checks = owner.get("check_ids") or []
        assertion_ids = [
            item.get("id") for item in assertions if isinstance(item, dict)
        ]
        if (
            not owner_id
            or owner_id in owner_ids
            or not artifact_ids
            or not checks
            or not assertion_ids
        ):
            raise HTTPException(
                422,
                "each owner needs a unique id and non-empty artifact/assertion/check scope",
            )
        if len(assertion_ids) != len(set(assertion_ids)) or any(
            path not in artifacts for path in artifact_ids
        ):
            raise HTTPException(422, f"{owner_id}: invalid assertion or artifact scope")
        owner_ids.add(owner_id)
    for artifact_id, artifact in artifacts.items():
        if (
            not isinstance(artifact, dict)
            or artifact.get("algorithm") != "sha256"
            or not _is_sha256(artifact.get("digest"))
        ):
            raise HTTPException(
                422, f"{artifact_id}: only sha256 artifacts are supported"
            )
    gate_ids: set[str] = set()
    for gate in gates:
        if not isinstance(gate, dict):
            raise HTTPException(422, "gate entries must be objects")
        required = gate.get("owners") or []
        if (
            gate.get("policy") != "all_required"
            or not gate.get("id")
            or gate["id"] in gate_ids
        ):
            raise HTTPException(
                422, "v1 gates require unique ids and all_required policy"
            )
        if (
            not required
            or len(required) != len(set(required))
            or not set(required) <= owner_ids
        ):
            raise HTTPException(422, f"{gate.get('id')}: gate owner set is invalid")
        gate_ids.add(gate["id"])
    return doc


def _is_sha256(value: Any) -> bool:
    return (
        isinstance(value, str)
        and len(value) == 64
        and all(c in "0123456789abcdef" for c in value)
    )


def owner_scope(contract: ReviewContract, owner_id: str) -> dict[str, Any]:
    for owner in contract.document["owners"]:
        if owner["id"] == owner_id:
            return owner
    raise HTTPException(422, f"unknown owner: {owner_id}")


def validate_receipt(contract: ReviewContract, body: Any) -> dict[str, Any]:
    if body.contract_digest != contract.contract_digest:
        raise HTTPException(
            409, "contract digest mismatch; re-review the active contract"
        )
    scope = owner_scope(contract, body.owner_id)
    if body.disposition == "request_changes":
        if not body.note or not body.note.strip():
            raise HTTPException(422, "request_changes requires a note")
    else:
        expected_artifacts = {
            key: contract.document["artifacts"][key]["digest"]
            for key in scope["artifact_ids"]
        }
        if body.artifact_hashes != expected_artifacts:
            raise HTTPException(
                422, "approval artifact hashes do not exactly match the owner scope"
            )
        expected_assertions = {item["id"] for item in scope["assertions"]}
        if set(body.assertion_ids) != expected_assertions or len(
            body.assertion_ids
        ) != len(expected_assertions):
            raise HTTPException(
                422, "approval assertions do not exactly match the owner scope"
            )
        expected_checks = set(scope["check_ids"])
        actual_checks = {
            item.get("id") for item in body.checks if isinstance(item, dict)
        }
        if actual_checks != expected_checks or len(body.checks) != len(expected_checks):
            raise HTTPException(
                422, "approval checks do not exactly match the owner scope"
            )
        if any(
            item.get("passed") is not True or item.get("read_only") is not True
            for item in body.checks
        ):
            raise HTTPException(
                422, "every approval check must be passed and explicitly read_only"
            )
    return {
        "owner_id": body.owner_id,
        "actor": body.actor,
        "disposition": body.disposition,
        "contract_digest": body.contract_digest,
        "artifact_hashes": body.artifact_hashes,
        "assertion_ids": body.assertion_ids,
        "checks": body.checks,
        "note": body.note,
        "source": body.source.model_dump(),
    }


async def active_contract(session: AsyncSession, contract_key: str) -> ReviewContract:
    result = await session.execute(
        select(ReviewContract).where(
            ReviewContract.contract_key == contract_key, ReviewContract.active.is_(True)
        )
    )
    contract = result.scalar_one_or_none()
    if contract is None:
        raise HTTPException(404, f"active review contract not found: {contract_key}")
    return contract


async def aggregate(
    session: AsyncSession, contract: ReviewContract
) -> ReviewAggregateRead:
    rows = await session.execute(
        select(ReviewReceipt)
        .where(ReviewReceipt.contract_id == contract.id)
        .order_by(ReviewReceipt.submitted_at.desc(), ReviewReceipt.id.desc())
    )
    latest: dict[str, ReviewReceipt] = {}
    for receipt in rows.scalars():
        latest.setdefault(receipt.owner_id, receipt)
    prior = await session.execute(
        select(ReviewReceipt.owner_id)
        .join(ReviewContract)
        .where(
            ReviewContract.contract_key == contract.contract_key,
            ReviewContract.id != contract.id,
        )
        .distinct()
    )
    stale_owners = set(prior.scalars())
    owner_states, statuses = [], {}
    for owner in contract.document["owners"]:
        owner_id, receipt = owner["id"], latest.get(owner["id"])
        state = (
            receipt.disposition.replace("approve", "approved")
            if receipt
            else ("stale" if owner_id in stale_owners else "missing")
        )
        statuses[owner_id] = state
        owner_states.append(
            OwnerReviewState(
                owner_id=owner_id,
                status=state,
                receipt_id=receipt.id if receipt else None,
                receipt_digest=receipt.receipt_digest if receipt else None,
                submitted_at=receipt.submitted_at if receipt else None,
            )
        )
    gates = [
        GateReviewState(
            gate_id=gate["id"],
            policy="all_required",
            owners=gate["owners"],
            satisfied=all(statuses[owner] == "approved" for owner in gate["owners"]),
        )
        for gate in contract.document["gates"]
    ]
    return ReviewAggregateRead(
        contract_key=contract.contract_key,
        contract_digest=contract.contract_digest,
        satisfied=all(gate.satisfied for gate in gates),
        authorizes_execution=False,
        owners=owner_states,
        gates=gates,
    )


def utc_now() -> datetime:
    return datetime.now(timezone.utc)
