"""Courier — payload building + HTTP submission, with optional approval queue."""

from __future__ import annotations

import hashlib
import json
import os
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

import httpx
from pydantic import BaseModel, Field
from sqlalchemy import select, update

from psyc import db, log
from psyc.lines import ledger as ledger_line
from psyc.lines.route import BlockedRoute, Route, endpoint_for
from psyc.models import ApprovalStatus, Case, Outcome, PendingSubmission, SealedPackage, TLP
from psyc.result import Err, Ok, Result


_log = log.get(__name__)

DEFAULT_TIMEOUT = 10.0
SUBMITTER_IDENTITY = "psyc/courier@0.1"

_STATUS_TO_OUTCOME: Dict[str, Outcome] = {
    "submitted": Outcome.SUBMITTED,
    "acknowledged": Outcome.ACKNOWLEDGED,
    "actioned": Outcome.ACTIONED,
    "rejected": Outcome.REJECTED,
}


class Receipt(BaseModel):
    receipt_id: str
    destination: str
    status: str
    response_body: Dict[str, Any] = Field(default_factory=dict)


class SubmitResult(BaseModel):
    destination_name: str
    outcome: Outcome
    receipt_id: Optional[str] = None
    detail: Optional[str] = None


def build_payload(case: Case, payload_kind: str, sealed_pkg: Optional[SealedPackage] = None) -> Dict[str, Any]:
    if payload_kind == "sealed_evidence_package":
        if sealed_pkg is None:
            return {"case_id": case.case_id, "error": "no sealed package available"}
        return sealed_pkg.model_dump(mode="json")
    if payload_kind == "stix_indicators":
        return {
            "spec_version": "2.1",
            "type": "bundle",
            "case_id": case.case_id,
            "tlp": case.classification.tlp.value,
            "objects": [
                {
                    "type": "indicator",
                    "id": f"indicator--{case.case_id}-{i}",
                    "pattern": f"[url:value = '{u}']",
                    "valid_from": case.observed_at.isoformat(),
                }
                for i, u in enumerate(case.observables.urls)
            ],
        }
    if payload_kind == "malware_url_report":
        return {
            "case_id": case.case_id,
            "urls": case.observables.urls,
            "threat": case.source_metadata.get("threat", "malware_distribution"),
            "tags": [t for t in case.source_metadata.get("tags", "").split(",") if t],
        }
    if payload_kind == "ip_abuse_report":
        return {
            "case_id": case.case_id,
            "ips": case.observables.ips,
            "category": "malware_distribution",
            "comment": case.summary,
        }
    return {"case_id": case.case_id, "payload_kind": payload_kind}


def _hash_payload(payload: Dict[str, Any]) -> str:
    return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()


def submit(endpoint: str, payload: Dict[str, Any], timeout: float = DEFAULT_TIMEOUT) -> Result[Receipt, str]:
    try:
        with httpx.Client(timeout=timeout) as client:
            r = client.post(endpoint, json=payload)
            r.raise_for_status()
            body = r.json()
    except httpx.HTTPStatusError as exc:
        _log.warning("courier.http.error", endpoint=endpoint, status=exc.response.status_code)
        return Err(f"HTTP {exc.response.status_code}: {exc.response.text[:200]}")
    except httpx.RequestError as exc:
        _log.warning("courier.request.error", endpoint=endpoint, error=str(exc))
        return Err(f"network error: {exc}")
    except Exception as exc:
        _log.warning("courier.unknown.error", endpoint=endpoint, error=str(exc))
        return Err(f"submit failed: {exc}")
    _log.info("courier.submitted", endpoint=endpoint, receipt_id=body.get("receipt_id", ""))
    return Ok(Receipt(
        receipt_id=body.get("receipt_id", ""),
        destination=body.get("destination", ""),
        status=body.get("status", "submitted"),
        response_body=body,
    ))


def execute_blocked_routes(case: Case, blocked: List[BlockedRoute]) -> None:
    for b in blocked:
        ledger_line.write(
            case_id=case.case_id,
            destination=b.destination_name,
            payload_hash="",
            submitter_identity="psyc/route-planner@0.1",
            tlp=case.classification.tlp,
            outcome=Outcome.REJECTED,
            detail=b.reason,
        )


def _force_approval() -> bool:
    return os.environ.get("PSYC_REQUIRE_APPROVAL", "").lower() in ("1", "true", "yes")


def _enqueue_pending(case: Case, route: Route, payload: Dict[str, Any], payload_hash: str) -> int:
    now = datetime.now(timezone.utc).isoformat()
    stmt = db.pending.insert().values(
        case_id=case.case_id,
        destination_name=route.destination_name,
        payload_kind=route.payload_kind,
        payload_hash=payload_hash,
        payload_json=json.dumps(payload, sort_keys=True),
        tlp=case.classification.tlp.value,
        created_at=now,
        status=ApprovalStatus.PENDING.value,
    )
    with db.engine().begin() as conn:
        res = conn.execute(stmt)
        pid = int(res.inserted_primary_key[0])
    _log.info("courier.queued", case_id=case.case_id, destination=route.destination_name, pending_id=pid)
    return pid


def execute_routes(case: Case, routes: List[Route], sealed_pkg: Optional[SealedPackage] = None) -> List[SubmitResult]:
    results: List[SubmitResult] = []
    force = _force_approval()
    for r in routes:
        endpoint = endpoint_for(r.destination_name)
        if endpoint is None:
            ledger_line.write(
                case_id=case.case_id,
                destination=r.destination_name,
                payload_hash="",
                submitter_identity=SUBMITTER_IDENTITY,
                tlp=case.classification.tlp,
                outcome=Outcome.FAILED,
                detail="no endpoint configured",
            )
            results.append(SubmitResult(destination_name=r.destination_name, outcome=Outcome.FAILED, detail="no endpoint configured"))
            continue
        payload = build_payload(case, r.payload_kind, sealed_pkg)
        payload_hash = _hash_payload(payload)
        if r.requires_approval or force:
            pid = _enqueue_pending(case, r, payload, payload_hash)
            results.append(SubmitResult(
                destination_name=r.destination_name,
                outcome=Outcome.PENDING_APPROVAL,
                detail=f"pending_id={pid}",
            ))
            continue
        result = submit(endpoint, payload)
        if isinstance(result, Err):
            ledger_line.write(
                case_id=case.case_id,
                destination=r.destination_name,
                payload_hash=payload_hash,
                submitter_identity=SUBMITTER_IDENTITY,
                tlp=case.classification.tlp,
                outcome=Outcome.FAILED,
                detail=result.reason,
            )
            results.append(SubmitResult(destination_name=r.destination_name, outcome=Outcome.FAILED, detail=result.reason))
            continue
        receipt = result.value
        outcome = _STATUS_TO_OUTCOME.get(receipt.status, Outcome.SUBMITTED)
        ledger_line.write(
            case_id=case.case_id,
            destination=r.destination_name,
            payload_hash=payload_hash,
            submitter_identity=SUBMITTER_IDENTITY,
            tlp=case.classification.tlp,
            outcome=outcome,
            response_id=receipt.receipt_id,
        )
        results.append(SubmitResult(destination_name=r.destination_name, outcome=outcome, receipt_id=receipt.receipt_id))
    return results


def _row_to_pending(row: Any) -> PendingSubmission:
    return PendingSubmission(
        id=row.id,
        case_id=row.case_id,
        destination_name=row.destination_name,
        payload_kind=row.payload_kind,
        payload_hash=row.payload_hash,
        payload_json=row.payload_json,
        tlp=TLP(row.tlp),
        created_at=datetime.fromisoformat(row.created_at),
        status=ApprovalStatus(row.status),
        reviewer=row.reviewer,
        reviewed_at=datetime.fromisoformat(row.reviewed_at) if row.reviewed_at else None,
        reason=row.reason,
    )


def list_pending(status: Optional[ApprovalStatus] = ApprovalStatus.PENDING, limit: int = 200) -> List[PendingSubmission]:
    stmt = select(db.pending)
    if status is not None:
        stmt = stmt.where(db.pending.c.status == status.value)
    stmt = stmt.order_by(db.pending.c.created_at.desc()).limit(limit)
    with db.engine().connect() as conn:
        rows = conn.execute(stmt).fetchall()
    return [_row_to_pending(r) for r in rows]


def get_pending(pid: int) -> Result[PendingSubmission, str]:
    stmt = select(db.pending).where(db.pending.c.id == pid)
    with db.engine().connect() as conn:
        row = conn.execute(stmt).fetchone()
    if row is None:
        return Err(f"pending submission not found: {pid}")
    return Ok(_row_to_pending(row))


def pending_count(status: ApprovalStatus = ApprovalStatus.PENDING) -> int:
    from sqlalchemy import func as sa_func
    stmt = select(sa_func.count()).select_from(db.pending).where(db.pending.c.status == status.value)
    with db.engine().connect() as conn:
        return int(conn.execute(stmt).scalar_one())


def dispatch_pending(pid: int, reviewer: str = "operator") -> Result[SubmitResult, str]:
    """Approve and submit a pending entry — POST to destination, write ledger, mark approved."""
    pending_r = get_pending(pid)
    if isinstance(pending_r, Err):
        return Err(pending_r.reason)
    p = pending_r.value
    if p.status != ApprovalStatus.PENDING:
        return Err(f"pending submission {pid} is already {p.status.value}")
    endpoint = endpoint_for(p.destination_name)
    if endpoint is None:
        return Err(f"no endpoint configured for {p.destination_name}")
    payload = json.loads(p.payload_json)
    result = submit(endpoint, payload)
    now = datetime.now(timezone.utc).isoformat()
    if isinstance(result, Err):
        ledger_line.write(
            case_id=p.case_id,
            destination=p.destination_name,
            payload_hash=p.payload_hash,
            submitter_identity=SUBMITTER_IDENTITY,
            tlp=p.tlp,
            outcome=Outcome.FAILED,
            detail=result.reason,
        )
        with db.engine().begin() as conn:
            conn.execute(update(db.pending).where(db.pending.c.id == pid).values(
                status=ApprovalStatus.APPROVED.value,
                reviewer=reviewer,
                reviewed_at=now,
                reason=f"submit failed: {result.reason}",
            ))
        return Ok(SubmitResult(destination_name=p.destination_name, outcome=Outcome.FAILED, detail=result.reason))
    receipt = result.value
    outcome = _STATUS_TO_OUTCOME.get(receipt.status, Outcome.SUBMITTED)
    ledger_line.write(
        case_id=p.case_id,
        destination=p.destination_name,
        payload_hash=p.payload_hash,
        submitter_identity=SUBMITTER_IDENTITY,
        tlp=p.tlp,
        outcome=outcome,
        response_id=receipt.receipt_id,
        detail=f"approved_by={reviewer}",
    )
    with db.engine().begin() as conn:
        conn.execute(update(db.pending).where(db.pending.c.id == pid).values(
            status=ApprovalStatus.APPROVED.value,
            reviewer=reviewer,
            reviewed_at=now,
        ))
    _log.info("courier.approved", pending_id=pid, reviewer=reviewer, outcome=outcome.value)
    return Ok(SubmitResult(destination_name=p.destination_name, outcome=outcome, receipt_id=receipt.receipt_id))


def reject_pending(pid: int, reviewer: str = "operator", reason: str = "") -> Result[None, str]:
    """Reject a pending entry — write ledger reject row, mark rejected. Nothing leaves."""
    pending_r = get_pending(pid)
    if isinstance(pending_r, Err):
        return Err(pending_r.reason)
    p = pending_r.value
    if p.status != ApprovalStatus.PENDING:
        return Err(f"pending submission {pid} is already {p.status.value}")
    now = datetime.now(timezone.utc).isoformat()
    ledger_line.write(
        case_id=p.case_id,
        destination=p.destination_name,
        payload_hash=p.payload_hash,
        submitter_identity=SUBMITTER_IDENTITY,
        tlp=p.tlp,
        outcome=Outcome.REJECTED,
        detail=f"rejected_by={reviewer}: {reason}" if reason else f"rejected_by={reviewer}",
    )
    with db.engine().begin() as conn:
        conn.execute(update(db.pending).where(db.pending.c.id == pid).values(
            status=ApprovalStatus.REJECTED.value,
            reviewer=reviewer,
            reviewed_at=now,
            reason=reason or None,
        ))
    _log.info("courier.rejected", pending_id=pid, reviewer=reviewer)
    return Ok(None)
