"""Slack interactive handler for Autopilot approvals."""

from __future__ import annotations

import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict

from .approvals import ApprovalTokenStore, default_approval_ttl_seconds
from .contracts import ActorContext, PlanSource, PlanV1, ReceiptApprovedBy, ReceiptV1
from .executor import PlanExecutor
from . import planner as base_planner
from .pending_actions import build_pending_action, load_pending_action, write_pending_action
from .policy import AutonomyPolicy, AllowRule, RuleMatch, apply_policy_overlays, compute_policy_hash, load_policy
from .policy_overrides import write_allow_rule_overlay
from .receipts import ReceiptWriter
from .slack_identity import map_slack_user
from .state import resolve_state_dir
from .suppress_store import SuppressStore
from .tools import ToolRegistry, get_default_registry
from g_gremlin.scout.models import EvidenceRow, RecommendedAction, ScoutFinding, ScoutScanResult


def _parse_action_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
    actions = payload.get("actions") or []
    if not actions:
        raise ValueError("No actions found in payload.")
    action = actions[0]
    value = action.get("value") or "{}"
    data = json.loads(value)
    return {
        "action_id": action.get("action_id"),
        "value": data,
        "slack_user_id": (payload.get("user") or {}).get("id"),
        "channel_id": (payload.get("channel") or {}).get("id"),
    }


def _load_plan(state_dir: Path, run_id: str) -> PlanV1:
    candidates = [
        Path(state_dir) / "pending" / run_id / "plan.json",
        Path(state_dir) / "plans" / f"{run_id}.json",
        Path(state_dir) / "receipts" / run_id / "plan.json",
    ]
    for path in candidates:
        if path.exists():
            with open(path, encoding="utf-8") as f:
                return PlanV1.from_dict(json.load(f))
    raise FileNotFoundError(f"Plan not found for run_id={run_id}")


def _load_scan(state_dir: Path, run_id: str) -> ScoutScanResult:
    candidates = [
        Path(state_dir) / "receipts" / run_id / "scan.json",
    ]
    for path in candidates:
        if path.exists():
            with open(path, encoding="utf-8") as f:
                return ScoutScanResult.from_dict(json.load(f))
    raise FileNotFoundError(f"Scan not found for run_id={run_id}")


def _get_policy(policy_path: Path | None) -> AutonomyPolicy:
    if policy_path and policy_path.exists():
        return load_policy(policy_path)
    raise FileNotFoundError("Policy file not found.")


def _get_state_dir(policy_path: Path | None, state_dir: Path | None) -> Path:
    if state_dir:
        return Path(state_dir).resolve()
    policy = _get_policy(policy_path)
    return resolve_state_dir(policy, create=True)


def _approval_store(state_dir: Path) -> ApprovalTokenStore:
    return ApprovalTokenStore(Path(state_dir) / "db" / "approval_tokens.sqlite")


def _build_allow_rule(plan: PlanV1, registry: ToolRegistry) -> AllowRule | None:
    tools = []
    tags = set()
    for step in plan.steps:
        tool_def = registry.get_tool(step.tool)
        if tool_def:
            tools.append(tool_def.name)
            tags.update(tool_def.policy_tags)
    if not tools and not tags:
        return None
    return AllowRule(
        name=f"slack_allow_{plan.run_id}",
        match=RuleMatch(tags=sorted(tags) or None, tools=sorted(set(tools)) or None),
        auto_execute=True,
        max_records=plan.risk.estimated_count if plan.risk else None,
    )


def _snooze_run(state_dir: Path, run_id: str, days: int = 7) -> Path:
    store = SuppressStore(Path(state_dir) / "db" / "suppress_store.sqlite")
    store.suppress_run(run_id, days=days, comment="slack_snooze")
    return Path(state_dir) / "db" / "suppress_store.sqlite"


def _approval_audit_log(state_dir: Path, payload: Dict[str, Any]) -> None:
    log_path = Path(state_dir) / "receipts" / "approval_audit.log"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    record = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        **payload,
    }
    with open(log_path, "a", encoding="utf-8") as f:
        f.write(json.dumps(record) + "\n")


def _user_has_approver_role(user_map: Dict[str, Any] | None, policy: AutonomyPolicy) -> bool:
    allowed_roles = [role.lower() for role in (policy.defaults.approver_roles or [])]
    if not allowed_roles:
        return True
    if not user_map:
        return False
    roles = user_map.get("roles")
    if roles is None:
        roles = user_map.get("role")
    if roles is None:
        return False
    if isinstance(roles, str):
        roles_list = [roles]
    elif isinstance(roles, list):
        roles_list = roles
    else:
        return False
    return any(str(role).lower() in allowed_roles for role in roles_list)


def handle_interactive(
    payload: Dict[str, Any],
    *,
    policy_path: Path | None = None,
    state_dir: Path | None = None,
) -> Dict[str, Any]:
    data = _parse_action_payload(payload)
    action_id = data["action_id"]
    value = data["value"]
    run_id = value.get("run_id", "")
    plan_hash = value.get("plan_hash", "")
    policy_hash = value.get("policy_hash", "")
    token = value.get("approve_token", "")

    resolved_state_dir = _get_state_dir(policy_path, state_dir)
    slack_user_id = data.get("slack_user_id") or ""
    user_map = map_slack_user(slack_user_id, resolved_state_dir)

    if action_id in ("autopilot_approve_run", "autopilot_always_allow", "autopilot_reject") and not user_map:
        _approval_audit_log(
            resolved_state_dir,
            {
                "action_id": action_id,
                "run_id": run_id,
                "slack_user_id": slack_user_id,
                "reason": "missing_slack_user_mapping",
            },
        )
        return {"status": "error", "reason": "missing_slack_user_mapping"}

    store = _approval_store(resolved_state_dir)

    if action_id == "autopilot_reject":
        policy = _get_policy(policy_path)
        if not _user_has_approver_role(user_map, policy):
            _approval_audit_log(
                resolved_state_dir,
                {
                    "action_id": action_id,
                    "run_id": run_id,
                    "slack_user_id": slack_user_id,
                    "reason": "unauthorized_role",
                },
            )
            return {"status": "error", "reason": "unauthorized_role"}
        consumed = store.consume_token(
            token,
            plan_hash,
            policy_hash,
            run_id,
            metadata={"action": "reject", "slack_user_id": slack_user_id},
        )
        return {"status": "rejected" if consumed else "invalid_token"}

    if action_id == "dealdesk_view_packets":
        index_url = value.get("index_url") or ""
        index_path = value.get("index_path") or ""
        return {"status": "ok", "url": index_url or index_path}

    if action_id == "dealdesk_snooze":
        items = value.get("snooze_items") or []
        evidence_rows = []
        for item in items:
            record_id = item.get("record_id")
            reason_codes = item.get("reason_codes") or []
            if not reason_codes:
                reason_codes = ["DEALDESK_CLOSE_VERIFICATION"]
            for code in reason_codes:
                if record_id and code:
                    evidence_rows.append(
                        EvidenceRow(
                            record_id=str(record_id),
                            sobject="Opportunity",
                            reason_code=str(code),
                        )
                    )
        if evidence_rows:
            finding = ScoutFinding(
                scout_name="dealdesk_close_verification",
                evidence=evidence_rows,
                findings_count=len(evidence_rows),
            )
            store = SuppressStore(Path(resolved_state_dir) / "db" / "suppress_store.sqlite")
            store.suppress_findings([finding], days=7, comment="dealdesk_snooze")
        return {"status": "snoozed", "count": len(evidence_rows)}

    if action_id == "dealdesk_create_tasks":
        policy = _get_policy(policy_path)
        policy = apply_policy_overlays(policy, resolved_state_dir)
        try:
            scan = _load_scan(resolved_state_dir, run_id)
        except Exception as exc:
            return {"status": "error", "reason": f"scan_not_found:{exc}"}
        rows: list[EvidenceRow] = []
        for finding in scan.findings:
            if not finding.scout_name.startswith("dealdesk"):
                continue
            for row in finding.evidence:
                fields = row.fields or {}
                if fields.get("overall_status") != "fail":
                    continue
                base_reason = row.reason or "Deal Desk verification failed"
                name = fields.get("Name") or fields.get("opportunity_name") or ""
                reason = f"{name}: {base_reason}" if name else base_reason
                reason_code = row.reason_code or (
                    (fields.get("reason_codes") or [None])[0] or "DEALDESK_CLOSE_VERIFICATION"
                )
                rows.append(
                    EvidenceRow(
                        record_id=row.record_id,
                        sobject=row.sobject or "Opportunity",
                        reason=reason,
                        reason_code=reason_code,
                        fields=row.fields,
                    )
                )
        if not rows:
            return {"status": "noop", "reason": "no_failures"}
        task_finding = ScoutFinding(
            scout_name="dealdesk_close_verification",
            display_name="Deal Desk Close Verification",
            evidence=rows,
            findings_count=len(rows),
        )
        action = RecommendedAction(
            action_type="tasks.create",
            label="Create tasks for Deal Desk failures",
            risk_level="low",
            details={"subject": "Deal Desk follow-up"},
        )
        registry = get_default_registry()
        tool_def = registry.get_tool("tasks.create")
        step = base_planner._build_step(  # type: ignore[attr-defined]
            step_id="s1",
            run_id=run_id,
            finding=task_finding,
            action=action,
            tool_name="tasks.create",
            tool_def=tool_def,
        )
        risk = base_planner._compute_plan_risk(  # type: ignore[attr-defined]
            [step],
            [tool_def],
            rows,
        )
        plan = PlanV1(
            version=1,
            run_id=run_id,
            source=PlanSource(type="scout_scan", scan_id=run_id),
            summary=f"Deal Desk: create tasks for {len(rows)} failure(s)",
            risk=risk,
            plan_hash=None,
            policy_hash=None,
            steps=[step],
            verification=[],
            approvals_required=risk.max_risk_level >= 1,
        )
        plan.policy_hash = compute_policy_hash(policy)
        executor = PlanExecutor(policy=policy, registry=registry, state_dir=resolved_state_dir)
        receipt = executor.execute(plan)
        writer = ReceiptWriter(resolved_state_dir)
        receipt_dir = writer.write(receipt, plan)

        approval_token = None
        pending_plan_path = None
        if receipt.outcome == "approval_required":
            pending_dir = resolved_state_dir / "pending" / plan.run_id
            pending_dir.mkdir(parents=True, exist_ok=True)
            pending_plan_path = pending_dir / "plan.json"
            with open(pending_plan_path, "w", encoding="utf-8") as handle:
                json.dump(plan.to_dict(), handle, indent=2)
            write_pending_action(
                resolved_state_dir,
                build_pending_action(
                    plan=plan,
                    actor=ActorContext(
                        actor_id=str(slack_user_id or "slack:unknown"),
                        roles=["approver"],
                        auth_source="slack",
                        mapped_email=str((user_map or {}).get("email") or "").strip() or None,
                        principal_type="user",
                        verified_identity=True,
                    ),
                ),
            )
            store = ApprovalTokenStore(resolved_state_dir / "db" / "approval_tokens.sqlite")
            approval_token = store.create_token(
                plan_hash=plan.plan_hash or "",
                policy_hash=plan.policy_hash or "",
                run_id=plan.run_id,
                ttl_seconds=default_approval_ttl_seconds(),
            ).token

        return {
            "status": receipt.outcome,
            "run_id": plan.run_id,
            "receipt_dir": str(receipt_dir),
            "approval_token": approval_token,
            "pending_plan_path": str(pending_plan_path) if pending_plan_path else None,
        }

    if action_id == "autopilot_snooze_7d":
        path = _snooze_run(resolved_state_dir, run_id, days=7)
        return {"status": "snoozed", "path": str(path)}

    policy = _get_policy(policy_path)
    policy = apply_policy_overlays(policy, resolved_state_dir)
    registry = get_default_registry()

    if action_id == "autopilot_always_allow":
        if not _user_has_approver_role(user_map, policy):
            _approval_audit_log(
                resolved_state_dir,
                {
                    "action_id": action_id,
                    "run_id": run_id,
                    "slack_user_id": slack_user_id,
                    "reason": "unauthorized_role",
                },
            )
            return {"status": "error", "reason": "unauthorized_role"}
        plan = _load_plan(resolved_state_dir, run_id)
        rule = _build_allow_rule(plan, registry)
        if not rule:
            return {"status": "error", "reason": "no_tools_for_allow_rule"}
        overlay_path = write_allow_rule_overlay(resolved_state_dir, rule)
        return {"status": "allow_written", "path": str(overlay_path)}

    if action_id in ("autopilot_approve_run", "dealdesk_approve_apply"):
        if not _user_has_approver_role(user_map, policy):
            _approval_audit_log(
                resolved_state_dir,
                {
                    "action_id": action_id,
                    "run_id": run_id,
                    "slack_user_id": slack_user_id,
                    "reason": "unauthorized_role",
                },
            )
            return {"status": "error", "reason": "unauthorized_role"}
        plan = _load_plan(resolved_state_dir, run_id)
        if plan.plan_hash != plan_hash:
            return {"status": "error", "reason": "plan_hash_mismatch"}
        current_policy_hash = compute_policy_hash(policy)
        if policy_hash and current_policy_hash != policy_hash:
            return {"status": "error", "reason": "policy_hash_mismatch"}
        pending_action = load_pending_action(resolved_state_dir, run_id)
        if pending_action is not None:
            maker_actor = str(pending_action.created_by.actor_id or "").strip()
            maker_email = str(pending_action.created_by.mapped_email or "").strip().lower()
            approver_email = str((user_map or {}).get("email") or "").strip().lower()
            if maker_actor and maker_actor == slack_user_id:
                return {"status": "error", "reason": "maker_checker_violation"}
            if maker_email and approver_email and maker_email == approver_email:
                return {"status": "error", "reason": "maker_checker_violation"}

        consumed = store.consume_token(
            token,
            plan_hash,
            current_policy_hash,
            run_id,
            metadata={"action": "approve", "slack_user_id": slack_user_id},
        )
        if not consumed:
            return {"status": "error", "reason": "invalid_token"}

        executor = PlanExecutor(policy=policy, registry=registry, state_dir=resolved_state_dir)
        receipt = executor.execute(plan, approve_token=token)
        if receipt.approved_by is None:
            receipt.approved_by = ReceiptApprovedBy(
                channel="slack",
                actor=slack_user_id,
                mapped_email=(user_map or {}).get("email"),
                principal_type="user",
                verified_identity=True,
            )
        elif receipt.approved_by.actor != slack_user_id:
            return {"status": "error", "reason": "approved_by_immutable_violation"}
        writer = ReceiptWriter(resolved_state_dir)
        receipt_dir = writer.write(receipt, plan)
        return {
            "status": "executed",
            "outcome": receipt.outcome,
            "run_id": receipt.run_id,
            "receipt_dir": str(receipt_dir),
        }

    return {"status": "ignored", "action_id": action_id}


__all__ = ["handle_interactive"]
