"""Scout scan to Autopilot Plan builder."""

from __future__ import annotations

import re
from typing import Any, Iterable

from g_gremlin.scout.models import EvidenceRow, RecommendedAction, ScoutFinding, ScoutScanResult

from .contracts import PlanRisk, PlanSource, PlanStep, PlanV1
from .tools import AutopilotToolDefinition, ToolRegistry


_RISK_LEVEL_MAP = {
    "safe": 0,
    "low": 1,
    "medium": 2,
    "high": 3,
}

_BLAST_ORDER = {
    "single_record": 0,
    "bulk_records": 1,
    "metadata": 2,
    "external_side_effect": 3,
}

_SFID_RE = re.compile(r"[A-Za-z0-9]{15,18}")
_ACTION_ALIASES = {
    "create_task": "tasks.create",
    "lead_to_account_match_health": "scout.lead_to_account_match_health",
    "deactivate_user": "sfdc.user.deactivate",
    "reassign_owner": "sfdc.record.reassign_owner",
    "dealdesk_renewal_create": "dealdesk.renewal.create_shell",
    "dealdesk_stamp": "dealdesk.approval.stamp",
    "opportunity_field_update": "sfdc.record.update",
}


def _action_risk_level(action: RecommendedAction, tool_def: AutopilotToolDefinition | None) -> int:
    if tool_def is None:
        return 3
    if tool_def.risk_level is not None:
        return int(tool_def.risk_level)
    raw = action.risk_level if action and action.risk_level is not None else ""
    if isinstance(raw, (int, float)):
        return max(0, min(3, int(raw)))
    if isinstance(raw, str):
        return _RISK_LEVEL_MAP.get(raw.strip().lower(), 3)
    return 3


def _build_evidence_payload(rows: Iterable[EvidenceRow]) -> list[dict[str, Any]]:
    payload = []
    for row in rows:
        payload.append({
            "record_id": row.record_id,
            "sobject": row.sobject,
            "fields": row.fields,
            "reason": row.reason,
            "reason_code": row.reason_code,
            "severity": row.severity,
            "related_ids": list(row.related_ids),
        })
    return payload


def _build_step_args(
    finding: ScoutFinding,
    action: RecommendedAction,
    evidence_rows: list[EvidenceRow],
    tool_name: str,
) -> dict[str, Any]:
    record_ids = [row.record_id for row in evidence_rows if row.record_id]
    payload = {
        "scout_name": finding.scout_name,
        "action_type": action.action_type,
        "action_details": action.details,
        "record_ids": record_ids,
        "records": _build_evidence_payload(evidence_rows),
    }

    if tool_name == "sfdc.user.assign_permission_sets":
        user_ids = _extract_user_ids(evidence_rows, record_ids)
        permission_set_ids = _extract_permission_set_ids(finding, action, evidence_rows)
        payload["user_ids"] = user_ids
        payload["permission_set_ids"] = permission_set_ids

    if tool_name == "sfdc.user.assign_permission_set_groups":
        user_ids = _extract_user_ids(evidence_rows, record_ids)
        group_ids = _extract_permission_set_group_ids(finding, action, evidence_rows)
        payload["user_ids"] = user_ids
        payload["permission_set_group_ids"] = group_ids

    if tool_name == "sfdc.user.add_to_queue_group":
        user_ids = _extract_user_ids(evidence_rows, record_ids)
        group_ids = _extract_queue_group_ids(finding, action, evidence_rows)
        payload["user_ids"] = user_ids
        payload["group_ids"] = group_ids

    if tool_name == "sfdc.forecast.assign_user_to_role":
        user_ids = _extract_user_ids(evidence_rows, record_ids)
        role_ids = _extract_forecast_role_ids(finding, action, evidence_rows)
        payload["user_ids"] = user_ids
        payload["role_ids"] = role_ids
        if isinstance(action.details, dict) and action.details.get("assignments"):
            payload["assignments"] = action.details.get("assignments")

    if tool_name == "tasks.create":
        owner_id = _extract_owner_id(finding, evidence_rows)
        if owner_id:
            payload["owner_id"] = owner_id
        payload["subject"] = _render_task_subject(finding, action)
        payload["description"] = _render_task_description(finding, action, evidence_rows)
        due_date = _extract_due_date(finding, action, evidence_rows)
        if due_date:
            payload["due_date"] = due_date

    if tool_name == "sfdc.record.update":
        if evidence_rows:
            payload["record_id"] = evidence_rows[0].record_id
            details = action.details or {}
            if isinstance(details, dict) and details.get("sobject"):
                payload["sobject"] = details.get("sobject")
            elif evidence_rows[0].sobject:
                payload["sobject"] = evidence_rows[0].sobject
            else:
                payload["sobject"] = "Opportunity"
            row_fields = evidence_rows[0].fields if evidence_rows[0].fields else {}
            payload["fields"] = row_fields.get("proposed_diffs", {})

    return payload


def _as_list(value: Any) -> list[str]:
    if value is None:
        return []
    if isinstance(value, list):
        return [str(item) for item in value if item not in (None, "")]
    return [str(value)]


def _dedupe(values: list[str]) -> list[str]:
    seen = set()
    ordered: list[str] = []
    for value in values:
        if value in seen:
            continue
        seen.add(value)
        ordered.append(value)
    return ordered


def _extract_permission_set_ids(
    finding: ScoutFinding,
    action: RecommendedAction,
    evidence_rows: list[EvidenceRow],
) -> list[str]:
    ids: list[str] = []
    details = action.details or {}
    if isinstance(details, dict):
        ids.extend(_as_list(details.get("permission_set_ids")))
        ids.extend(_as_list(details.get("permission_set_id")))
        ids.extend(_as_list(details.get("permission_set_names")))

    params_used = finding.parameters_used or {}
    if isinstance(params_used, dict):
        ids.extend(_as_list(params_used.get("permission_set_ids")))
        ids.extend(_as_list(params_used.get("permission_set_id")))
        ids.extend(_as_list(params_used.get("permission_set_names")))

    for row in evidence_rows:
        fields = row.fields or {}
        if isinstance(fields, dict):
            for key in ("permission_set_id", "PermissionSetId", "permissionSetId"):
                if key in fields:
                    ids.extend(_as_list(fields.get(key)))
        reason_code = row.reason_code or ""
        if "permission" in reason_code.lower():
            ids.extend(_SFID_RE.findall(reason_code))

    return _dedupe(ids)


def _extract_user_ids(evidence_rows: list[EvidenceRow], record_ids: list[str]) -> list[str]:
    user_ids = [
        row.record_id
        for row in evidence_rows
        if row.record_id and (row.sobject or "").lower() == "user"
    ]
    if not user_ids:
        return record_ids
    return user_ids


def _extract_permission_set_group_ids(
    finding: ScoutFinding,
    action: RecommendedAction,
    evidence_rows: list[EvidenceRow],
) -> list[str]:
    ids: list[str] = []
    details = action.details or {}
    if isinstance(details, dict):
        ids.extend(_as_list(details.get("permission_set_group_ids")))
        ids.extend(_as_list(details.get("permission_set_group_id")))
        ids.extend(_as_list(details.get("permission_set_group_names")))

    params_used = finding.parameters_used or {}
    if isinstance(params_used, dict):
        ids.extend(_as_list(params_used.get("permission_set_group_ids")))
        ids.extend(_as_list(params_used.get("permission_set_group_id")))
        ids.extend(_as_list(params_used.get("permission_set_group_names")))

    for row in evidence_rows:
        fields = row.fields or {}
        if isinstance(fields, dict):
            for key in ("PermissionSetGroupId", "permission_set_group_id"):
                if key in fields:
                    ids.extend(_as_list(fields.get(key)))
        reason_code = row.reason_code or ""
        if "group" in reason_code.lower():
            ids.extend(_SFID_RE.findall(reason_code))

    return _dedupe(ids)


def _extract_queue_group_ids(
    finding: ScoutFinding,
    action: RecommendedAction,
    evidence_rows: list[EvidenceRow],
) -> list[str]:
    ids: list[str] = []
    details = action.details or {}
    if isinstance(details, dict):
        ids.extend(_as_list(details.get("group_ids")))
        ids.extend(_as_list(details.get("group_id")))
        ids.extend(_as_list(details.get("queue_group_ids")))
        ids.extend(_as_list(details.get("queue_group_id")))
        ids.extend(_as_list(details.get("group_names")))
        ids.extend(_as_list(details.get("queue_names")))

    params_used = finding.parameters_used or {}
    if isinstance(params_used, dict):
        ids.extend(_as_list(params_used.get("group_ids")))
        ids.extend(_as_list(params_used.get("group_id")))
        ids.extend(_as_list(params_used.get("queue_group_ids")))
        ids.extend(_as_list(params_used.get("queue_group_id")))
        ids.extend(_as_list(params_used.get("group_names")))
        ids.extend(_as_list(params_used.get("queue_names")))

    for row in evidence_rows:
        fields = row.fields or {}
        if isinstance(fields, dict):
            for key in ("GroupId", "QueueId", "group_id", "queue_id"):
                if key in fields:
                    ids.extend(_as_list(fields.get(key)))
        reason_code = row.reason_code or ""
        if "queue" in reason_code.lower() or "group" in reason_code.lower():
            ids.extend(_SFID_RE.findall(reason_code))

    return _dedupe(ids)


def _extract_forecast_role_ids(
    finding: ScoutFinding,
    action: RecommendedAction,
    evidence_rows: list[EvidenceRow],
) -> list[str]:
    ids: list[str] = []
    details = action.details or {}
    if isinstance(details, dict):
        ids.extend(_as_list(details.get("role_ids")))
        ids.extend(_as_list(details.get("role_id")))
        ids.extend(_as_list(details.get("forecast_role_ids")))
        ids.extend(_as_list(details.get("forecast_role_id")))

    params_used = finding.parameters_used or {}
    if isinstance(params_used, dict):
        ids.extend(_as_list(params_used.get("role_ids")))
        ids.extend(_as_list(params_used.get("role_id")))
        ids.extend(_as_list(params_used.get("forecast_role_ids")))
        ids.extend(_as_list(params_used.get("forecast_role_id")))

    for row in evidence_rows:
        fields = row.fields or {}
        if isinstance(fields, dict):
            for key in ("ForecastRoleId", "forecast_role_id", "RoleId", "role_id"):
                if key in fields:
                    ids.extend(_as_list(fields.get(key)))
        reason_code = row.reason_code or ""
        if "forecast" in reason_code.lower() or "role" in reason_code.lower():
            ids.extend(_SFID_RE.findall(reason_code))

    return _dedupe(ids)


def _resolve_tool_name(action_type: str) -> str:
    return _ACTION_ALIASES.get(action_type, action_type)


def _render_task_subject(finding: ScoutFinding, action: RecommendedAction) -> str:
    details = action.details or {}
    if isinstance(details, dict) and details.get("subject"):
        return str(details["subject"])
    name = finding.display_name or finding.scout_name
    return f"Autopilot follow-up: {name}"


def _render_task_description(
    finding: ScoutFinding,
    action: RecommendedAction,
    evidence_rows: list[EvidenceRow],
) -> str:
    details = action.details or {}
    if isinstance(details, dict) and details.get("description"):
        return str(details["description"])
    if evidence_rows:
        row = evidence_rows[0]
        if row.reason:
            return str(row.reason)
        if row.reason_code:
            return f"Reason: {row.reason_code}"
    return f"Generated from scout {finding.scout_name}"


def _extract_owner_id(finding: ScoutFinding, evidence_rows: list[EvidenceRow]) -> str | None:
    details = finding.parameters_used or {}
    if isinstance(details, dict):
        owner = (
            details.get("owner_id")
            or details.get("ownerId")
            or details.get("OwnerId")
            or details.get("task_owner_id")
        )
        if owner:
            return str(owner)
    for row in evidence_rows:
        fields = row.fields or {}
        if isinstance(fields, dict):
            owner = fields.get("OwnerId") or fields.get("owner_id") or fields.get("ownerid")
            if owner:
                return str(owner)
    return None


def _extract_due_date(
    finding: ScoutFinding,
    action: RecommendedAction,
    evidence_rows: list[EvidenceRow],
) -> str | None:
    details = action.details or {}
    if isinstance(details, dict):
        due = details.get("due_date") or details.get("dueDate") or details.get("ActivityDate")
        if due:
            return str(due)
    for row in evidence_rows:
        fields = row.fields or {}
        if isinstance(fields, dict):
            due = fields.get("ActivityDate") or fields.get("due_date") or fields.get("DueDate")
            if due:
                return str(due)
    return None


def _best_effort_diff_preview(evidence_rows: list[EvidenceRow]) -> dict[str, Any] | None:
    if not evidence_rows:
        return None
    row = evidence_rows[0]
    if not row.fields:
        return None
    return {
        "record_id": row.record_id,
        "sobject": row.sobject,
        "fields": row.fields,
        "reason_code": row.reason_code,
    }


def _build_step(
    *,
    step_id: str,
    run_id: str,
    finding: ScoutFinding,
    action: RecommendedAction,
    tool_name: str,
    tool_def: AutopilotToolDefinition | None,
) -> PlanStep:
    evidence_rows = list(finding.evidence)
    risk_level = _action_risk_level(action, tool_def)
    args = _build_step_args(finding, action, evidence_rows, tool_name)
    return PlanStep(
        step_id=step_id,
        tool=tool_name,
        diff_preview=_best_effort_diff_preview(evidence_rows),
        args=args,
        risk_level=risk_level,
        idempotency_key=f"{run_id}:{step_id}",
        prechecks=list(tool_def.default_prechecks) if tool_def else [],
        postchecks=list(tool_def.default_postchecks) if tool_def else [],
        evidence_ids=[f"{finding.scout_name}:{idx}" for idx, _ in enumerate(evidence_rows)],
    )


def _resolve_reassignment_config(
    finding: ScoutFinding,
    action: RecommendedAction,
) -> dict[str, Any]:
    details = action.details or {}
    if isinstance(details, dict) and isinstance(details.get("reassignment_config"), dict):
        return details.get("reassignment_config") or {}
    params_used = finding.parameters_used or {}
    if isinstance(params_used, dict) and isinstance(params_used.get("reassignment_config"), dict):
        return params_used.get("reassignment_config") or {}
    return {}


def _build_reassign_steps(
    *,
    run_id: str,
    step_index: int,
    finding: ScoutFinding,
    action: RecommendedAction,
    tool_name: str,
    tool_def: AutopilotToolDefinition | None,
) -> tuple[list[PlanStep], int]:
    config = _resolve_reassignment_config(finding, action)
    if not config:
        return [], step_index
    config_map = {str(k).lower(): v for k, v in config.items() if isinstance(v, dict)}
    grouped: dict[str, dict[str, Any]] = {}
    for row in finding.evidence:
        if not row.sobject:
            continue
        key = str(row.sobject).lower()
        cfg = config_map.get(key)
        if not isinstance(cfg, dict):
            continue
        new_owner_id = cfg.get("default_owner_id") or cfg.get("default_queue_id")
        if not new_owner_id:
            continue
        group = grouped.setdefault(
            key,
            {"sobject": row.sobject, "new_owner_id": new_owner_id, "rows": []},
        )
        group["rows"].append(row)

    steps: list[PlanStep] = []
    for group in grouped.values():
        evidence_rows = list(group["rows"])
        risk_level = _action_risk_level(action, tool_def)
        args = _build_step_args(finding, action, evidence_rows, tool_name)
        args["sobject"] = group["sobject"]
        args["new_owner_id"] = group["new_owner_id"]
        step_id = f"s{step_index}"
        step_index += 1
        steps.append(
            PlanStep(
                step_id=step_id,
                tool=tool_name,
                diff_preview=_best_effort_diff_preview(evidence_rows),
                args=args,
                risk_level=risk_level,
                idempotency_key=f"{run_id}:{step_id}",
                prechecks=list(tool_def.default_prechecks) if tool_def else [],
                postchecks=list(tool_def.default_postchecks) if tool_def else [],
                evidence_ids=[f"{finding.scout_name}:{idx}" for idx, _ in enumerate(evidence_rows)],
            )
        )
    return steps, step_index


def _summarize_plan(findings: list[ScoutFinding], steps: list[PlanStep]) -> str:
    if not steps:
        return "No executable actions found in scout scan."
    actionable_findings = [f for f in findings if f.recommended_actions]
    if len(actionable_findings) == 1:
        finding = actionable_findings[0]
        name = finding.display_name or finding.scout_name
        return f"{name}: {finding.findings_count} finding(s) -> {len(steps)} step(s)"
    return f"{len(steps)} step(s) from {len(actionable_findings)} finding(s)"


def _compute_plan_risk(
    steps: list[PlanStep],
    tool_defs: list[AutopilotToolDefinition | None],
    evidence_rows: list[EvidenceRow],
) -> PlanRisk:
    max_risk = 0
    irreversible = False
    blast_radius = "single_record"

    for step, tool_def in zip(steps, tool_defs):
        max_risk = max(max_risk, step.risk_level)
        if tool_def and tool_def.risk_category in ("irreversible", "metadata_write"):
            irreversible = True
        if step.risk_level >= 3:
            irreversible = True
        radius = tool_def.blast_radius if tool_def else None
        if radius and _BLAST_ORDER.get(radius, 0) > _BLAST_ORDER.get(blast_radius, 0):
            blast_radius = radius

    estimated_count = len([row for row in evidence_rows if row.record_id])
    target_object = None
    sobjects = {row.sobject for row in evidence_rows if row.sobject}
    if len(sobjects) == 1:
        target_object = next(iter(sobjects))

    return PlanRisk(
        max_risk_level=max_risk,
        irreversible=irreversible,
        blast_radius=blast_radius,
        estimated_count=estimated_count or None,
        target_object=target_object,
    )


def build_plan(scan_result: ScoutScanResult, registry: ToolRegistry) -> PlanV1:
    run_id = scan_result.run_id
    steps: list[PlanStep] = []
    step_tool_defs: list[AutopilotToolDefinition | None] = []
    evidence_rows: list[EvidenceRow] = []
    verification_targets: list[str] = []
    verification_seen: set[str] = set()
    step_index = 1

    for finding in scan_result.findings:
        if not finding.recommended_actions:
            continue
        if finding.scout_name and finding.scout_name not in verification_seen:
            verification_seen.add(finding.scout_name)
            verification_targets.append(finding.scout_name)
        evidence_rows.extend(list(finding.evidence))
        for action in finding.recommended_actions:
            tool_name = _resolve_tool_name(action.action_type)
            tool_def = registry.get_tool(tool_name)
            if tool_name == "sfdc.record.reassign_owner":
                new_steps, step_index = _build_reassign_steps(
                    run_id=run_id,
                    step_index=step_index,
                    finding=finding,
                    action=action,
                    tool_name=tool_name,
                    tool_def=tool_def,
                )
                for step in new_steps:
                    steps.append(step)
                    step_tool_defs.append(tool_def)
                continue
            step_id = f"s{step_index}"
            step_index += 1
            steps.append(
                _build_step(
                    step_id=step_id,
                    run_id=run_id,
                    finding=finding,
                    action=action,
                    tool_name=tool_name,
                    tool_def=tool_def,
                )
            )
            step_tool_defs.append(tool_def)

    summary = _summarize_plan(scan_result.findings, steps)
    risk = _compute_plan_risk(steps, step_tool_defs, evidence_rows)
    approvals_required = risk.max_risk_level >= 1

    plan = PlanV1(
        version=1,
        run_id=run_id,
        source=PlanSource(type="scout_scan", scan_id=scan_result.run_id),
        summary=summary,
        risk=risk,
        plan_hash=None,
        policy_hash=None,
        steps=steps,
        verification=verification_targets,
        approvals_required=approvals_required,
    )
    return plan


__all__ = ["build_plan"]
