import hashlib
import json
import os
import time
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple

import psycopg
from psycopg.rows import dict_row


PG_DSN = os.getenv("CLAN_PG_DSN", "postgresql://daarion:DaarionDB2026!@dagi-postgres:5432/daarion_main")
BATCH_SIZE = int(os.getenv("CLAN_OUTBOX_BATCH_SIZE", "10"))
POLL_INTERVAL = float(os.getenv("CLAN_OUTBOX_POLL_INTERVAL_SEC", "1.0"))
MAX_ARTIFACT_CAS_RETRIES = int(os.getenv("CLAN_OUTBOX_MAX_CAS_RETRIES", "5"))
APPLIER_ACTOR_ID = os.getenv("CLAN_CONSENT_APPLIER_ACTOR_ID", "system:consent-applier")

CONSENT_TRANSITION_MAP = {
    "bridge_request_draft": {
        "approve": ("approved_for_execution", "export_validated"),
        "reject": ("rejected", "validated"),
        "revoke": ("revoked", "corrected"),
    },
    "allocation_proposal": {
        "approve": ("approved_for_execution", "validated"),
        "reject": ("rejected", "validated"),
        "revoke": ("revoked", "corrected"),
    },
    "access_grant_draft": {
        "approve": ("approved_for_execution", "policy_checked"),
        "reject": ("rejected", "validated"),
        "revoke": ("revoked", "corrected"),
    },
    "visibility_change_draft": {
        "approve": ("approved_for_execution", "policy_checked"),
        "reject": ("rejected", "validated"),
        "revoke": ("revoked", "corrected"),
    },
    "offline_merge_plan": {
        "approve": ("approved_for_execution", "merged"),
        "reject": ("rejected", "validated"),
        "revoke": ("revoked", "corrected"),
    },
    "core_change_draft": {
        "approve": ("needs_confirmation", "policy_checked"),
        "reject": ("rejected", "validated"),
        "revoke": ("revoked", "corrected"),
    },
    "testimony_draft": {
        "approve": ("confirmed", "validated"),
        "reject": ("rejected", "validated"),
        "revoke": ("revoked", "corrected"),
    },
}


def _id(prefix: str, seed: str) -> str:
    h = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:20]
    return f"{prefix}_{h}"


def _now_ts() -> int:
    return int(time.time())


def _provenance_has_consent(provenance: Any, consent_event_id: str) -> bool:
    if not isinstance(provenance, list):
        return False
    for tr in provenance:
        ctx = (tr or {}).get("context") or {}
        op = ((tr or {}).get("operation") or {}).get("op")
        if ctx.get("consent_event_ref") == consent_event_id and op in {
            "validated",
            "export_validated",
            "policy_checked",
            "merged",
            "corrected",
        }:
            return True
    return False


def _build_trail(
    consent_event: Dict[str, Any],
    request_id: str,
    consent_event_id: str,
    decision_type: str,
    op: str,
    visibility_level: str,
    versions: Dict[str, Any],
) -> Dict[str, Any]:
    return {
        "event_id": _id("prov", f"{consent_event_id}:{op}:{_now_ts()}"),
        "ts": _now_ts(),
        "actor": {"type": "system", "id": APPLIER_ACTOR_ID},
        "source": {"channel": "internal", "request_id": request_id},
        "context": {
            "visibility_level": visibility_level,
            "consent_status": "confirmed" if decision_type == "approve" else "none",
            "consent_event_ref": consent_event_id,
        },
        "operation": {"op": op},
        "versions": versions,
        "links": {},
    }


@dataclass
class OutboxRow:
    outbox_id: str
    consent_event_id: str
    target_artifact_ids: List[str]
    request_id: str


def _fetch_pending_outbox(cur, limit: int) -> List[OutboxRow]:
    cur.execute(
        """
        select outbox_id, consent_event_id, target_artifact_ids, request_id
        from clan.clan_outbox
        where status='pending'
        order by created_ts
        limit %s
        for update skip locked
        """,
        (limit,),
    )
    rows = cur.fetchall() or []
    return [
        OutboxRow(
            outbox_id=r["outbox_id"],
            consent_event_id=r["consent_event_id"],
            target_artifact_ids=list(r["target_artifact_ids"] or []),
            request_id=r["request_id"],
        )
        for r in rows
    ]


def _set_outbox_error(cur, outbox_id: str, error: str) -> None:
    cur.execute(
        """
        update clan.clan_outbox
        set attempts = attempts + 1,
            last_error = %s,
            updated_ts = now()
        where outbox_id = %s
        """,
        (error[:800], outbox_id),
    )


def _mark_outbox_done(cur, outbox_id: str) -> None:
    cur.execute(
        """
        update clan.clan_outbox
        set status='done', updated_ts=now()
        where outbox_id=%s and status='pending'
        """,
        (outbox_id,),
    )


def _get_consent_event(cur, consent_event_id: str) -> Optional[Dict[str, Any]]:
    cur.execute(
        "select payload from clan.clan_consent_events where consent_event_id=%s",
        (consent_event_id,),
    )
    row = cur.fetchone()
    return row["payload"] if row else None


def _get_artifact(cur, artifact_id: str) -> Optional[Dict[str, Any]]:
    cur.execute(
        """
        select artifact_id, artifact_type, status, visibility_level, payload, provenance, version
        from clan.clan_artifacts
        where artifact_id=%s
        """,
        (artifact_id,),
    )
    return cur.fetchone()


def _cas_update_artifact(
    cur,
    artifact_id: str,
    expected_version: int,
    new_status: str,
    new_payload: Dict[str, Any],
    new_provenance: Any,
) -> bool:
    cur.execute(
        """
        update clan.clan_artifacts
        set status=%s,
            payload=%s::jsonb,
            provenance=%s::jsonb,
            version=version+1,
            updated_ts=now()
        where artifact_id=%s and version=%s
        """,
        (
            new_status,
            json.dumps(new_payload, ensure_ascii=False),
            json.dumps(new_provenance, ensure_ascii=False),
            artifact_id,
            expected_version,
        ),
    )
    return cur.rowcount == 1


def _insert_transition(
    cur,
    *,
    consent_event_id: str,
    decision_type: str,
    request_id: str,
    artifact_id: str,
    artifact_type: str,
    visibility_level: str,
    from_status: str,
    to_status: str,
    op: str,
    versions: Dict[str, Any],
) -> None:
    transition_id = _id("tr", f"{consent_event_id}:{artifact_id}:{decision_type}:{to_status}:{op}")
    cur.execute(
        """
        insert into clan.clan_state_transitions(
          transition_id, artifact_id, artifact_type, from_status, to_status, op,
          consent_event_id, decision_type, request_id, visibility_level, versions
        ) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s::jsonb)
        on conflict (transition_id) do nothing
        """,
        (
            transition_id,
            artifact_id,
            artifact_type,
            from_status,
            to_status,
            op,
            consent_event_id,
            decision_type,
            request_id,
            visibility_level,
            json.dumps(versions, ensure_ascii=False),
        ),
    )


def _validate_consent_min(consent_event: Dict[str, Any]) -> Tuple[bool, str]:
    try:
        decision_type = consent_event["decision"]["type"]
        if decision_type not in {"approve", "reject", "revoke"}:
            return False, "invalid decision.type"
        target_ids = (consent_event.get("target") or {}).get("artifact_ids") or []
        if not target_ids:
            return False, "empty target.artifact_ids"

        expires_at = (consent_event.get("decision") or {}).get("expires_at")
        if expires_at is not None and int(expires_at) < _now_ts():
            return False, "consent expired"

        if decision_type == "approve":
            confirmations = consent_event.get("confirmations") or []
            quorum = consent_event.get("quorum") or {}
            required = int(quorum.get("required", 1))
            present = int(quorum.get("present", 0))
            if len(confirmations) < required or present < required:
                return False, "quorum not met"
        return True, ""
    except Exception as e:
        return False, f"validation_error:{e}"


def _apply_outbox(cur, outbox: OutboxRow) -> None:
    consent = _get_consent_event(cur, outbox.consent_event_id)
    if not consent:
        _set_outbox_error(cur, outbox.outbox_id, "STOP_CONSENT_EVENT_MISSING: consent not found")
        return

    ok, err = _validate_consent_min(consent)
    if not ok:
        _set_outbox_error(cur, outbox.outbox_id, f"STOP_CONSENT_EVENT_INVALID: {err}")
        return

    decision_type = (consent.get("decision") or {}).get("type")
    versions = {
        "constitution_version": str((consent.get("versions") or {}).get("constitution_version") or "unknown"),
        "protocol_version": str((consent.get("versions") or {}).get("protocol_version") or "unknown"),
        "router_guard_version": str((consent.get("versions") or {}).get("router_guard_version") or "unknown"),
    }

    for artifact_id in outbox.target_artifact_ids:
        artifact = _get_artifact(cur, artifact_id)
        if not artifact:
            _set_outbox_error(cur, outbox.outbox_id, f"STOP_CONSENT_EVENT_MISSING: artifact {artifact_id} not found")
            return

        if artifact["artifact_type"] not in CONSENT_TRANSITION_MAP:
            _set_outbox_error(cur, outbox.outbox_id, f"STOP_CONSENT_EVENT_INVALID: no transition for {artifact['artifact_type']}")
            return

        if decision_type not in CONSENT_TRANSITION_MAP[artifact["artifact_type"]]:
            _set_outbox_error(cur, outbox.outbox_id, f"STOP_CONSENT_EVENT_INVALID: decision not mapped {decision_type}")
            return

        from_status = artifact["status"]
        if from_status == "rejected" and decision_type == "approve":
            _set_outbox_error(cur, outbox.outbox_id, f"STOP_CONSENT_EVENT_INVALID: one-way violation {artifact_id}")
            return

        provenance = artifact["provenance"]
        if _provenance_has_consent(provenance, outbox.consent_event_id):
            continue

        to_status, op = CONSENT_TRANSITION_MAP[artifact["artifact_type"]][decision_type]
        trail = _build_trail(
            consent_event=consent,
            request_id=outbox.request_id,
            consent_event_id=outbox.consent_event_id,
            decision_type=decision_type,
            op=op,
            visibility_level=artifact["visibility_level"],
            versions=versions,
        )

        payload = artifact["payload"]
        expected_version = int(artifact["version"])
        new_provenance = (provenance if isinstance(provenance, list) else []) + [trail]

        success = False
        for _ in range(MAX_ARTIFACT_CAS_RETRIES):
            success = _cas_update_artifact(
                cur,
                artifact_id=artifact_id,
                expected_version=expected_version,
                new_status=to_status,
                new_payload=payload,
                new_provenance=new_provenance,
            )
            if success:
                break

            current = _get_artifact(cur, artifact_id)
            if not current:
                break
            expected_version = int(current["version"])
            payload = current["payload"]
            provenance = current["provenance"]
            if _provenance_has_consent(provenance, outbox.consent_event_id):
                success = True
                break
            new_provenance = (provenance if isinstance(provenance, list) else []) + [trail]

        if not success:
            _set_outbox_error(cur, outbox.outbox_id, f"CAS conflict exceeded for {artifact_id}")
            return

        _insert_transition(
            cur,
            consent_event_id=outbox.consent_event_id,
            decision_type=decision_type,
            request_id=outbox.request_id,
            artifact_id=artifact_id,
            artifact_type=artifact["artifact_type"],
            visibility_level=artifact["visibility_level"],
            from_status=from_status,
            to_status=to_status,
            op=op,
            versions=versions,
        )

    _mark_outbox_done(cur, outbox.outbox_id)


def main() -> None:
    while True:
        try:
            with psycopg.connect(PG_DSN, row_factory=dict_row) as conn:
                conn.autocommit = False
                with conn.cursor() as cur:
                    outboxes = _fetch_pending_outbox(cur, BATCH_SIZE)
                    if not outboxes:
                        conn.rollback()
                        time.sleep(POLL_INTERVAL)
                        continue
                    for ob in outboxes:
                        _apply_outbox(cur, ob)
                    conn.commit()
        except Exception:
            time.sleep(POLL_INTERVAL)


if __name__ == "__main__":
    main()
