"""Respondline — human-gated response actions (SOAR-lite).

High-severity cases propose response actions (alert the SOC, push IOCs to
enforcement, open a ticket). Nothing fires automatically: each action sits in
PROPOSED until a human approves it, mirroring the submission approval gate.
On approval the action is dispatched to the configured enforcement sink
(PSYC_SOAR_URL, default = the mock-cert container) and recorded in the ledger.
"""

from __future__ import annotations

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

import httpx
from sqlalchemy import func as sa_func
from sqlalchemy import select, update

from psyc import db, log
from psyc.lines import ledger as ledger_line
from psyc.models import ActionStatus, ActionType, Case, Outcome, ResponseAction, Severity, TLP
from psyc.result import Err, Ok, Result


_log = log.get(__name__)

SOAR_BASE = os.environ.get("PSYC_SOAR_URL", "http://127.0.0.1:8770")
SOAR_ENDPOINT = f"{SOAR_BASE}/soar/enforce"
DEFAULT_TIMEOUT = 10.0
APPROVER_DEFAULT = "operator"

# Only act on cases this severe or worse.
_ACTIONABLE = {Severity.HIGH, Severity.CRITICAL}


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def propose_for_case(case: Case) -> List[int]:
    """Generate response actions for a high-severity case. Returns new action ids.

    Idempotent per case: if actions already exist for this case, propose none.
    """
    if case.classification.severity not in _ACTIONABLE:
        return []
    if _action_count_for_case(case.case_id) > 0:
        return []

    sev = case.classification.severity.value
    obs = case.observables
    ioc_total = len(obs.ips) + len(obs.domains) + len(obs.urls) + len(obs.hashes)
    actions: List[ResponseAction] = []

    # 1. Alert the SOC.
    actions.append(ResponseAction(
        case_id=case.case_id,
        action_type=ActionType.ALERT,
        target="soc-webhook",
        summary=f"Alert SOC: {sev.upper()} {case.classification.incident_type.value if case.classification.incident_type else 'threat'} — {case.summary[:80]}",
        payload_json=json.dumps({
            "kind": "alert", "case_id": case.case_id, "severity": sev,
            "summary": case.summary, "ioc_count": ioc_total,
        }, ensure_ascii=False),
        severity=sev,
        created_at=datetime.now(timezone.utc),
    ))

    # 2. Push IOCs to enforcement, if there are any network indicators.
    if obs.ips or obs.domains or obs.urls:
        actions.append(ResponseAction(
            case_id=case.case_id,
            action_type=ActionType.BLOCKLIST,
            target="perimeter-firewall+dns",
            summary=f"Block {len(obs.ips)} IP(s), {len(obs.domains)} domain(s), {len(obs.urls)} URL(s) at the perimeter",
            payload_json=json.dumps({
                "kind": "blocklist", "case_id": case.case_id, "severity": sev,
                "ips": obs.ips, "domains": obs.domains, "urls": obs.urls,
            }, ensure_ascii=False),
            severity=sev,
            created_at=datetime.now(timezone.utc),
        ))

    ids: List[int] = []
    with db.engine().begin() as conn:
        for a in actions:
            res = conn.execute(db.response_actions.insert().values(
                case_id=a.case_id, action_type=a.action_type.value, target=a.target,
                summary=a.summary, payload_json=a.payload_json, severity=a.severity,
                status=ActionStatus.PROPOSED.value, created_at=a.created_at.isoformat(),
            ))
            ids.append(int(res.inserted_primary_key[0]))
    _log.info("respond.proposed", case_id=case.case_id, actions=len(ids))
    return ids


def _action_count_for_case(case_id: str) -> int:
    stmt = select(sa_func.count()).select_from(db.response_actions).where(db.response_actions.c.case_id == case_id)
    with db.engine().connect() as conn:
        return int(conn.execute(stmt).scalar_one())


def _row_to_action(row: Any) -> ResponseAction:
    return ResponseAction(
        id=row.id, case_id=row.case_id, action_type=ActionType(row.action_type),
        target=row.target, summary=row.summary, payload_json=row.payload_json,
        severity=row.severity, status=ActionStatus(row.status),
        created_at=datetime.fromisoformat(row.created_at),
        approver=row.approver,
        executed_at=datetime.fromisoformat(row.executed_at) if row.executed_at else None,
        detail=row.detail,
    )


def list_actions(status: Optional[ActionStatus] = None, limit: int = 200) -> List[ResponseAction]:
    stmt = select(db.response_actions)
    if status is not None:
        stmt = stmt.where(db.response_actions.c.status == status.value)
    stmt = stmt.order_by(db.response_actions.c.created_at.desc()).limit(limit)
    with db.engine().connect() as conn:
        return [_row_to_action(r) for r in conn.execute(stmt).fetchall()]


def get_action(action_id: int) -> Result[ResponseAction, str]:
    stmt = select(db.response_actions).where(db.response_actions.c.id == action_id)
    with db.engine().connect() as conn:
        row = conn.execute(stmt).fetchone()
    if row is None:
        return Err(f"action not found: {action_id}")
    return Ok(_row_to_action(row))


def action_count(status: ActionStatus) -> int:
    stmt = select(sa_func.count()).select_from(db.response_actions).where(db.response_actions.c.status == status.value)
    with db.engine().connect() as conn:
        return int(conn.execute(stmt).scalar_one())


def execute_action(action_id: int, approver: str = APPROVER_DEFAULT) -> Result[ResponseAction, str]:
    """Approve + fire an action: POST to the enforcement sink, ledger it, mark executed."""
    got = get_action(action_id)
    if isinstance(got, Err):
        return Err(got.reason)
    a = got.value
    if a.status != ActionStatus.PROPOSED:
        return Err(f"action {action_id} is already {a.status.value}")

    payload = json.loads(a.payload_json)
    payload["action_type"] = a.action_type.value
    payload["approved_by"] = approver
    try:
        with httpx.Client(timeout=DEFAULT_TIMEOUT) as client:
            resp = client.post(SOAR_ENDPOINT, json=payload)
            resp.raise_for_status()
            body = resp.json()
        receipt = str(body.get("receipt_id", ""))
        ok = True
        detail = f"enforced_by={approver} → {receipt}"
    except Exception as exc:  # noqa: BLE001 — network/sink failure is expected-path
        ok = False
        detail = f"enforcement failed: {exc}"
        _log.warning("respond.execute.error", action_id=action_id, error=str(exc))

    now = _now()
    new_status = ActionStatus.EXECUTED if ok else ActionStatus.FAILED
    ledger_line.write(
        case_id=a.case_id,
        destination=f"SOAR:{a.action_type.value}:{a.target}",
        payload_hash="",
        submitter_identity="psyc/respond@0.1",
        tlp=TLP.AMBER,
        outcome=Outcome.ACTIONED if ok else Outcome.FAILED,
        detail=detail,
    )
    with db.engine().begin() as conn:
        conn.execute(update(db.response_actions).where(db.response_actions.c.id == action_id).values(
            status=new_status.value, approver=approver, executed_at=now, detail=detail,
        ))
    _log.info("respond.executed", action_id=action_id, ok=ok, approver=approver)
    if not ok:
        return Err(detail)
    refreshed = get_action(action_id)
    return refreshed if isinstance(refreshed, Ok) else Err("post-execute read failed")


def reject_action(action_id: int, approver: str = APPROVER_DEFAULT, reason: str = "") -> Result[None, str]:
    """Decline a proposed action — nothing fires; ledger records the decision."""
    got = get_action(action_id)
    if isinstance(got, Err):
        return Err(got.reason)
    a = got.value
    if a.status != ActionStatus.PROPOSED:
        return Err(f"action {action_id} is already {a.status.value}")
    ledger_line.write(
        case_id=a.case_id,
        destination=f"SOAR:{a.action_type.value}:{a.target}",
        payload_hash="",
        submitter_identity="psyc/respond@0.1",
        tlp=TLP.AMBER,
        outcome=Outcome.REJECTED,
        detail=f"declined_by={approver}: {reason}" if reason else f"declined_by={approver}",
    )
    with db.engine().begin() as conn:
        conn.execute(update(db.response_actions).where(db.response_actions.c.id == action_id).values(
            status=ActionStatus.REJECTED.value, approver=approver, executed_at=_now(),
            detail=reason or None,
        ))
    _log.info("respond.rejected", action_id=action_id, approver=approver)
    return Ok(None)
