"""Verification runners for Autopilot plan execution."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import re
from typing import Any, Callable

from g_gremlin.scout.engine import ScoutEngine
from g_gremlin.scout.loader import load_scan_config
from g_gremlin.scout.models import ScoutFinding

from .contracts import PlanStep, PlanV1
from .policy import AutonomyPolicy
from .tools import AutopilotToolDefinition, ToolRegistry


PrecheckHandler = Callable[[PlanStep, AutopilotToolDefinition | None], "CheckResult"]
PostcheckHandler = Callable[
    [PlanStep, AutopilotToolDefinition | None, dict[str, Any] | None],
    "CheckResult",
]

_KNOWN_CHECK_NAMES = {
    "sfdc.record.exists",
    "sfdc.record.updated",
    "sfdc.user.exists",
    "sfdc.user.not_already_assigned",
    "sfdc.user.assignment_verified",
    "sfdc.group.exists",
    "sfdc.group.membership_verified",
    "sfdc.user.not_system_admin",
    "sfdc.user.deactivated",
    "sfdc.owner.exists_and_active",
    "sfdc.record.owner_updated",
    "sfdc.role.exists",
    "sfdc.forecast.hierarchy_verified",
    "tasks.created",
    "sequence.pause.applied",
}

_SOBJECT_DEFAULTS_BY_TOOL = {
    "sfdc.record.update": "Opportunity",
    "sfdc.record.enrich_missing_fields": "Account",
    "sfdc.followup.enforce": "Event",
}

_SOQL_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*$")


@dataclass
class CheckResult:
    check_name: str
    passed: bool
    detail: str = ""


class InvalidSoqlIdentifierError(ValueError):
    """Raised when dynamic SOQL identifier context fails allowlist validation."""


def _resolve_check_names(step: PlanStep, tool_def: AutopilotToolDefinition | None) -> list[str]:
    if step.prechecks:
        return list(step.prechecks)
    if tool_def and tool_def.default_prechecks:
        return list(tool_def.default_prechecks)
    return []


def _resolve_postcheck_names(step: PlanStep, tool_def: AutopilotToolDefinition | None) -> list[str]:
    if step.postchecks:
        return list(step.postchecks)
    if tool_def and tool_def.default_postchecks:
        return list(tool_def.default_postchecks)
    return []


def _pass(check_name: str, detail: str = "") -> CheckResult:
    return CheckResult(check_name=check_name, passed=True, detail=detail)


def _fail(check_name: str, detail: str) -> CheckResult:
    return CheckResult(check_name=check_name, passed=False, detail=detail)


def _skip(check_name: str, detail: str) -> CheckResult:
    return CheckResult(check_name=check_name, passed=True, detail=f"skipped: {detail}")


def run_prechecks(step: PlanStep, tool_def: AutopilotToolDefinition | None) -> list[CheckResult]:
    results: list[CheckResult] = []
    for name in _resolve_check_names(step, tool_def):
        handler = _PRECHECKS.get(name)
        if handler is None:
            results.append(_fail(name, "unknown precheck"))
            continue
        try:
            results.append(handler(step, tool_def))
        except InvalidSoqlIdentifierError as exc:
            results.append(_fail(name, f"invalid soql identifier: {exc}"))
    return results


def run_postchecks(
    step: PlanStep,
    tool_def: AutopilotToolDefinition | None,
    *,
    result: dict[str, Any] | None = None,
) -> list[CheckResult]:
    names = _resolve_postcheck_names(step, tool_def)
    if not names:
        return []
    if _is_dry_run_result(result):
        return [_skip(name, "dry run result") for name in names]
    results: list[CheckResult] = []
    for name in names:
        handler = _POSTCHECKS.get(name)
        if handler is None:
            results.append(_fail(name, "unknown postcheck"))
            continue
        try:
            results.append(handler(step, tool_def, result))
        except InvalidSoqlIdentifierError as exc:
            results.append(_fail(name, f"invalid soql identifier: {exc}"))
    return results


def _is_dry_run_result(result: dict[str, Any] | None) -> bool:
    after = result.get("after") if isinstance(result, dict) else None
    return isinstance(after, dict) and str(after.get("status") or "").strip().lower() == "dry_run"


def _step_details(step: PlanStep) -> dict[str, Any]:
    details = step.args.get("action_details")
    return details if isinstance(details, dict) else {}


def _resolve_org_alias(step: PlanStep) -> str | None:
    value = step.args.get("org_alias")
    if value not in (None, ""):
        return str(value)
    details = _step_details(step)
    fallback = details.get("org_alias")
    if fallback not in (None, ""):
        return str(fallback)
    return None


def _resolve_sobject(step: PlanStep) -> str | None:
    for key in ("sobject", "target_sobject"):
        value = step.args.get(key)
        if value not in (None, ""):
            return str(value)
    details = _step_details(step)
    for key in ("sobject", "target_sobject"):
        value = details.get(key)
        if value not in (None, ""):
            return str(value)
    return _SOBJECT_DEFAULTS_BY_TOOL.get(step.tool)


def _as_list(value: Any) -> list[str]:
    if value is None:
        return []
    if isinstance(value, list):
        return [str(item).strip() for item in value if str(item).strip()]
    cleaned = str(value).strip()
    return [cleaned] if cleaned else []


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


def _extract_record_ids(step: PlanStep, *, result: dict[str, Any] | None = None) -> list[str]:
    values: list[str] = []
    args = step.args or {}
    values.extend(_as_list(args.get("record_id")))
    values.extend(_as_list(args.get("record_ids")))
    values.extend(_as_list(args.get("targets")))
    values.extend(_as_list(args.get("ids")))
    records = args.get("records")
    if isinstance(records, list):
        for row in records:
            if not isinstance(row, dict):
                continue
            values.extend(_as_list(row.get("record_id")))
    if isinstance(result, dict):
        values.extend(_as_list(result.get("targets")))
    return _dedupe(values)


def _ensure_valid_soql_identifiers(
    *,
    sobject: str | None = None,
    fields: list[str] | None = None,
) -> None:
    invalid: list[str] = []
    if sobject not in (None, ""):
        candidate = str(sobject).strip()
        if _SOQL_IDENTIFIER_RE.fullmatch(candidate) is None:
            invalid.append(f"sobject={candidate!r}")
    for raw_field in fields or []:
        candidate = str(raw_field).strip()
        if _SOQL_IDENTIFIER_RE.fullmatch(candidate) is None:
            invalid.append(f"field={candidate!r}")
    if invalid:
        raise InvalidSoqlIdentifierError(", ".join(invalid))


def _extract_user_ids(step: PlanStep) -> list[str]:
    args = step.args or {}
    values: list[str] = []
    if step.tool in {
        "sfdc.user.assign_permission_sets",
        "sfdc.user.assign_permission_set_groups",
        "sfdc.user.add_to_queue_group",
        "sfdc.user.deactivate",
        "sfdc.forecast.assign_user_to_role",
    }:
        values.extend(_as_list(args.get("user_ids")))
        values.extend(_as_list(args.get("record_ids")))
        values.extend(_as_list(args.get("user_id")))
    if step.tool in {"tasks.create", "sfdc.followup.enforce"}:
        values.extend(_as_list(args.get("owner_id")))
        records = args.get("records")
        if isinstance(records, list):
            for row in records:
                if not isinstance(row, dict):
                    continue
                fields = row.get("fields") if isinstance(row.get("fields"), dict) else {}
                values.extend(_as_list(fields.get("OwnerId")))
    assignments = args.get("assignments") or args.get("role_assignments")
    if isinstance(assignments, list):
        for assignment in assignments:
            if not isinstance(assignment, dict):
                continue
            values.extend(_as_list(assignment.get("user_id")))
    return _dedupe(values)


def _extract_permission_targets(step: PlanStep) -> list[str]:
    args = step.args or {}
    values: list[str] = []
    if step.tool == "sfdc.user.assign_permission_sets":
        for key in ("permission_set_ids", "permission_set_id", "permission_set_names"):
            values.extend(_as_list(args.get(key)))
            values.extend(_as_list(_step_details(step).get(key)))
    if step.tool == "sfdc.user.assign_permission_set_groups":
        for key in (
            "permission_set_group_ids",
            "permission_set_group_id",
            "permission_set_group_names",
        ):
            values.extend(_as_list(args.get(key)))
            values.extend(_as_list(_step_details(step).get(key)))
    return _dedupe(values)


def _extract_group_targets(step: PlanStep) -> list[str]:
    args = step.args or {}
    values: list[str] = []
    for key in ("group_ids", "group_id", "queue_group_ids", "queue_group_id", "group_names", "queue_names"):
        values.extend(_as_list(args.get(key)))
        values.extend(_as_list(_step_details(step).get(key)))
    return _dedupe(values)


def _extract_role_targets(step: PlanStep) -> list[str]:
    args = step.args or {}
    values: list[str] = []
    for key in ("role_ids", "role_id", "forecast_role_id", "forecast_role_ids"):
        values.extend(_as_list(args.get(key)))
        values.extend(_as_list(_step_details(step).get(key)))
    assignments = args.get("assignments") or args.get("role_assignments")
    if isinstance(assignments, list):
        for assignment in assignments:
            if not isinstance(assignment, dict):
                continue
            values.extend(_as_list(assignment.get("role_id")))
    return _dedupe(values)


def _extract_new_owner_id(step: PlanStep) -> str | None:
    args = step.args or {}
    for key in ("new_owner_id", "owner_id"):
        value = args.get(key)
        if value not in (None, ""):
            return str(value)
    details = _step_details(step)
    for key in ("new_owner_id", "owner_id", "default_owner_id"):
        value = details.get(key)
        if value not in (None, ""):
            return str(value)
    return None


def _extract_assignment_pairs(
    step: PlanStep,
    result: dict[str, Any] | None,
) -> list[tuple[str, str]]:
    args = step.args or {}
    after = result.get("after") if isinstance(result, dict) else {}
    entries = after.get("assignments") if isinstance(after, dict) else None
    pairs: list[tuple[str, str]] = []
    if isinstance(entries, list):
        for entry in entries:
            if not isinstance(entry, dict):
                continue
            user_id = str(entry.get("user_id") or "").strip()
            target = str(
                entry.get("permission_set_id")
                or entry.get("permission_set_group_id")
                or entry.get("role_id")
                or ""
            ).strip()
            if user_id and target:
                pairs.append((user_id, target))
    if pairs:
        return pairs

    user_ids = _extract_user_ids(step)
    if step.tool in {
        "sfdc.user.assign_permission_sets",
        "sfdc.user.assign_permission_set_groups",
    }:
        targets = _extract_permission_targets(step)
        return [(user_id, target) for user_id in user_ids for target in targets]
    if step.tool == "sfdc.forecast.assign_user_to_role":
        role_ids = _extract_role_targets(step)
        if len(role_ids) == 1:
            return [(user_id, role_ids[0]) for user_id in user_ids]
        if len(role_ids) == len(user_ids):
            return list(zip(user_ids, role_ids))
    return []


def _extract_group_membership_pairs(
    step: PlanStep,
    result: dict[str, Any] | None,
) -> list[tuple[str, str]]:
    after = result.get("after") if isinstance(result, dict) else {}
    entries = after.get("memberships") if isinstance(after, dict) else None
    pairs: list[tuple[str, str]] = []
    if isinstance(entries, list):
        for entry in entries:
            if not isinstance(entry, dict):
                continue
            user_id = str(entry.get("user_id") or "").strip()
            group_id = str(entry.get("group_id") or "").strip()
            if user_id and group_id:
                pairs.append((user_id, group_id))
    if pairs:
        return pairs
    return [
        (user_id, group_id)
        for user_id in _extract_user_ids(step)
        for group_id in _extract_group_targets(step)
    ]


def _run_soql(query: str, *, org_alias: str | None = None) -> list[dict[str, Any]]:
    from g_gremlin import sfdc as sfdc_helpers

    rows = sfdc_helpers.run_soql(query, org_alias=org_alias)
    if not isinstance(rows, list):
        return []
    return [row for row in rows if isinstance(row, dict)]


def _escape_soql(value: Any) -> str:
    from g_gremlin import sfdc as sfdc_helpers

    return sfdc_helpers.escape_soql_value(str(value))


def _in_clause(values: list[str]) -> str:
    escaped = [f"'{_escape_soql(value)}'" for value in values if str(value).strip()]
    return ", ".join(escaped)


def _query_by_ids(
    *,
    sobject: str,
    fields: list[str],
    ids: list[str],
    org_alias: str | None,
) -> list[dict[str, Any]]:
    if not ids:
        return []
    _ensure_valid_soql_identifiers(sobject=sobject, fields=fields)
    select_fields = ", ".join(dict.fromkeys(["Id", *fields]))
    query = f"SELECT {select_fields} FROM {sobject} WHERE Id IN ({_in_clause(ids)})"
    return _run_soql(query, org_alias=org_alias)


def _check_sfdc_record_exists(step: PlanStep, _tool_def: AutopilotToolDefinition | None) -> CheckResult:
    check_name = "sfdc.record.exists"
    sobject = _resolve_sobject(step)
    if not sobject:
        return _skip(check_name, "missing sobject context")
    record_ids = _extract_record_ids(step)
    if not record_ids:
        return _skip(check_name, "missing record ids")
    rows = _query_by_ids(sobject=sobject, fields=[], ids=record_ids, org_alias=_resolve_org_alias(step))
    found = {str(row.get("Id")) for row in rows if row.get("Id")}
    missing = [record_id for record_id in record_ids if record_id not in found]
    if missing:
        return _fail(check_name, f"missing records: {missing}")
    return _pass(check_name, f"verified {len(found)} record(s)")


def _check_sfdc_user_exists(step: PlanStep, _tool_def: AutopilotToolDefinition | None) -> CheckResult:
    check_name = "sfdc.user.exists"
    user_ids = _extract_user_ids(step)
    if not user_ids:
        return _skip(check_name, "missing user ids")
    rows = _query_by_ids(
        sobject="User",
        fields=["IsActive", "Profile.Name"],
        ids=user_ids,
        org_alias=_resolve_org_alias(step),
    )
    found = {str(row.get("Id")) for row in rows if row.get("Id")}
    missing = [user_id for user_id in user_ids if user_id not in found]
    if missing:
        return _fail(check_name, f"missing users: {missing}")
    return _pass(check_name, f"verified {len(found)} user(s)")


def _check_sfdc_user_not_already_assigned(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
) -> CheckResult:
    check_name = "sfdc.user.not_already_assigned"
    pairs = _extract_assignment_pairs(step, result=None)
    if not pairs:
        return _skip(check_name, "missing assignment targets")

    org_alias = _resolve_org_alias(step)
    if step.tool == "sfdc.user.assign_permission_sets":
        target_field = "PermissionSetId"
        sobject = "PermissionSetAssignment"
    elif step.tool == "sfdc.user.assign_permission_set_groups":
        target_field = "PermissionSetGroupId"
        sobject = "PermissionSetGroupAssignment"
    else:
        return _skip(check_name, f"unsupported tool {step.tool}")

    user_ids = _dedupe([user_id for user_id, _target in pairs])
    target_ids = _dedupe([target for _user_id, target in pairs])
    query = (
        f"SELECT AssigneeId, {target_field} "
        f"FROM {sobject} "
        f"WHERE AssigneeId IN ({_in_clause(user_ids)}) "
        f"AND {target_field} IN ({_in_clause(target_ids)})"
    )
    rows = _run_soql(query, org_alias=org_alias)
    existing = {
        (str(row.get("AssigneeId")), str(row.get(target_field)))
        for row in rows
        if row.get("AssigneeId") and row.get(target_field)
    }
    duplicates = [f"{user_id}:{target}" for user_id, target in pairs if (user_id, target) in existing]
    if duplicates:
        return _fail(check_name, f"existing assignments: {duplicates}")
    return _pass(check_name, f"verified {len(pairs)} assignment target(s)")


def _check_sfdc_group_exists(step: PlanStep, _tool_def: AutopilotToolDefinition | None) -> CheckResult:
    check_name = "sfdc.group.exists"
    targets = _extract_group_targets(step)
    if not targets:
        return _skip(check_name, "missing group identifiers")
    _ensure_valid_soql_identifiers(sobject="Group", fields=["Id", "Name"])
    query = (
        "SELECT Id, Name "
        "FROM Group "
        f"WHERE Id IN ({_in_clause(targets)}) OR Name IN ({_in_clause(targets)})"
    )
    rows = _run_soql(query, org_alias=_resolve_org_alias(step))
    seen = set()
    for row in rows:
        if row.get("Id"):
            seen.add(str(row.get("Id")))
        if row.get("Name"):
            seen.add(str(row.get("Name")))
    missing = [target for target in targets if target not in seen]
    if missing:
        return _fail(check_name, f"missing groups: {missing}")
    return _pass(check_name, f"verified {len(targets)} group target(s)")


def _check_sfdc_user_not_system_admin(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
) -> CheckResult:
    check_name = "sfdc.user.not_system_admin"
    user_ids = _extract_user_ids(step)
    if not user_ids:
        return _skip(check_name, "missing user ids")
    rows = _query_by_ids(
        sobject="User",
        fields=["Profile.Name"],
        ids=user_ids,
        org_alias=_resolve_org_alias(step),
    )
    by_id = {str(row.get("Id")): row for row in rows if row.get("Id")}
    missing = [user_id for user_id in user_ids if user_id not in by_id]
    if missing:
        return _fail(check_name, f"missing users: {missing}")
    admins = []
    for user_id, row in by_id.items():
        profile_name = str(row.get("Profile.Name") or "")
        if "system administrator" in profile_name.lower():
            admins.append(user_id)
    if admins:
        return _fail(check_name, f"system administrators cannot be auto-managed: {admins}")
    return _pass(check_name, f"verified {len(user_ids)} user(s) are not system administrators")


def _check_sfdc_owner_exists_and_active(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
) -> CheckResult:
    check_name = "sfdc.owner.exists_and_active"
    owner_id = _extract_new_owner_id(step)
    if not owner_id:
        return _skip(check_name, "missing new owner id")
    org_alias = _resolve_org_alias(step)
    user_rows = _query_by_ids(
        sobject="User",
        fields=["IsActive", "Profile.Name"],
        ids=[owner_id],
        org_alias=org_alias,
    )
    if user_rows:
        row = user_rows[0]
        if str(row.get("IsActive")).strip().lower() in {"false", "0"}:
            return _fail(check_name, f"user {owner_id} is inactive")
        return _pass(check_name, f"verified active user owner {owner_id}")
    query = (
        "SELECT Id, Type "
        "FROM Group "
        f"WHERE Id = '{_escape_soql(owner_id)}' AND Type = 'Queue'"
    )
    queue_rows = _run_soql(query, org_alias=org_alias)
    if queue_rows:
        return _pass(check_name, f"verified queue owner {owner_id}")
    return _fail(check_name, f"owner {owner_id} not found as active user or queue")


def _check_sfdc_role_exists(step: PlanStep, _tool_def: AutopilotToolDefinition | None) -> CheckResult:
    check_name = "sfdc.role.exists"
    role_ids = _extract_role_targets(step)
    if not role_ids:
        return _skip(check_name, "missing role identifiers")
    _ensure_valid_soql_identifiers(sobject="UserRole", fields=["Id", "Name", "DeveloperName"])
    query = (
        "SELECT Id, Name, DeveloperName "
        "FROM UserRole "
        f"WHERE Id IN ({_in_clause(role_ids)}) OR Name IN ({_in_clause(role_ids)}) OR DeveloperName IN ({_in_clause(role_ids)})"
    )
    rows = _run_soql(query, org_alias=_resolve_org_alias(step))
    seen = set()
    for row in rows:
        for key in ("Id", "Name", "DeveloperName"):
            value = row.get(key)
            if value not in (None, ""):
                seen.add(str(value))
    missing = [role_id for role_id in role_ids if role_id not in seen]
    if missing:
        return _fail(check_name, f"missing roles: {missing}")
    return _pass(check_name, f"verified {len(role_ids)} role target(s)")


def _extract_expected_record_updates(
    step: PlanStep,
    result: dict[str, Any] | None,
) -> dict[str, dict[str, Any]]:
    expected: dict[str, dict[str, Any]] = {}
    args = step.args or {}
    if step.tool == "sfdc.record.update":
        record_id = args.get("record_id")
        fields = args.get("fields") if isinstance(args.get("fields"), dict) else None
        if record_id and fields:
            expected[str(record_id)] = {
                str(field_name): _coerce_expected_field_value(field_value)
                for field_name, field_value in fields.items()
            }
        records = args.get("records")
        if isinstance(records, list):
            for row in records:
                if not isinstance(row, dict):
                    continue
                rid = str(row.get("record_id") or "").strip()
                row_fields = row.get("fields") if isinstance(row.get("fields"), dict) else {}
                if isinstance(row_fields, dict) and isinstance(row_fields.get("proposed_diffs"), dict):
                    row_fields = row_fields.get("proposed_diffs") or {}
                if rid and row_fields:
                    expected[rid] = {
                        str(field_name): _coerce_expected_field_value(field_value)
                        for field_name, field_value in row_fields.items()
                    }
        return expected

    if step.tool == "sfdc.record.enrich_missing_fields" and isinstance(result, dict):
        after = result.get("after") if isinstance(result.get("after"), dict) else {}
        updated_records = after.get("updated_records")
        if isinstance(updated_records, list):
            for entry in updated_records:
                if not isinstance(entry, dict):
                    continue
                rid = str(entry.get("record_id") or "").strip()
                updates = entry.get("updates") if isinstance(entry.get("updates"), dict) else {}
                if rid and updates:
                    expected[rid] = {
                        str(field_name): _coerce_expected_field_value(field_value)
                        for field_name, field_value in updates.items()
                    }
    return expected


def _extract_task_entries(result: dict[str, Any] | None) -> list[dict[str, Any]]:
    after = result.get("after") if isinstance(result, dict) else {}
    if not isinstance(after, dict):
        return []
    entries = after.get("created")
    if isinstance(entries, list):
        return [entry for entry in entries if isinstance(entry, dict)]
    entries = after.get("created_tasks")
    if isinstance(entries, list):
        return [entry for entry in entries if isinstance(entry, dict)]
    return []


def _extract_task_ids(entries: list[dict[str, Any]]) -> list[str]:
    task_ids: list[str] = []
    for entry in entries:
        candidates: list[Any] = []
        candidates.append(entry.get("task_id"))
        candidates.append(entry.get("id"))
        result_payload = entry.get("result") if isinstance(entry.get("result"), dict) else {}
        task_payload = entry.get("task") if isinstance(entry.get("task"), dict) else {}
        candidates.append(result_payload.get("id"))
        candidates.append((result_payload.get("result") or {}).get("id") if isinstance(result_payload.get("result"), dict) else None)
        candidates.append(task_payload.get("id"))
        candidates.append((task_payload.get("result") or {}).get("id") if isinstance(task_payload.get("result"), dict) else None)
        for candidate in candidates:
            if candidate in (None, ""):
                continue
            value = str(candidate).strip()
            if value:
                task_ids.append(value)
                break
    return _dedupe(task_ids)


def _get_row_value(row: dict[str, Any], field_name: str) -> Any:
    if field_name in row:
        return row.get(field_name)
    lowered = field_name.lower()
    for key, value in row.items():
        if str(key).lower() == lowered:
            return value
    return None


def _normalize_scalar(value: Any) -> Any:
    if isinstance(value, str):
        return value.strip()
    return value


def _values_match(expected: Any, actual: Any) -> bool:
    expected_value = _normalize_scalar(expected)
    actual_value = _normalize_scalar(actual)
    if expected_value == actual_value:
        return True
    try:
        return float(expected_value) == float(actual_value)
    except Exception:
        return str(expected_value) == str(actual_value)


def _coerce_expected_field_value(value: Any) -> Any:
    if isinstance(value, dict):
        if "after" in value:
            return value.get("after")
        if "value" in value:
            return value.get("value")
    return value


def _contains_pause_tag(value: Any, tag_value: str) -> bool:
    if isinstance(value, list):
        return any(_contains_pause_tag(item, tag_value) for item in value)
    if value in (None, ""):
        return False
    text = str(value)
    parts = [part.strip() for part in text.split(";") if part.strip()]
    return tag_value in parts or tag_value == text.strip()


def _check_sfdc_record_updated(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
    result: dict[str, Any] | None,
) -> CheckResult:
    check_name = "sfdc.record.updated"
    if not isinstance(result, dict):
        return _fail(check_name, "missing execution result")
    after = result.get("after") if isinstance(result.get("after"), dict) else {}
    status = str(after.get("status") or "").strip().lower()
    if step.tool == "sfdc.record.enrich_missing_fields" and status in {
        "no_changes",
        "no_targets",
        "no_providers_configured",
    }:
        return _pass(check_name, f"status={status}")

    sobject = _resolve_sobject(step)
    if not sobject:
        return _skip(check_name, "missing sobject context")
    expected = _extract_expected_record_updates(step, result)
    if not expected:
        return _skip(check_name, "no expected field updates to verify")

    fields = sorted({field for updates in expected.values() for field in updates.keys() if field})
    rows = _query_by_ids(
        sobject=sobject,
        fields=fields,
        ids=list(expected.keys()),
        org_alias=_resolve_org_alias(step),
    )
    by_id = {str(row.get("Id")): row for row in rows if row.get("Id")}
    mismatches: list[str] = []
    missing = [record_id for record_id in expected.keys() if record_id not in by_id]
    if missing:
        return _fail(check_name, f"missing post-update records: {missing}")

    for record_id, updates in expected.items():
        row = by_id.get(record_id, {})
        for field_name, expected_value in updates.items():
            actual_value = _get_row_value(row, field_name)
            if not _values_match(expected_value, actual_value):
                mismatches.append(
                    f"{record_id}.{field_name}: expected={expected_value!r} actual={actual_value!r}"
                )
    if mismatches:
        return _fail(check_name, "; ".join(mismatches[:5]))
    return _pass(check_name, f"verified {len(expected)} updated record(s)")


def _check_sfdc_user_assignment_verified(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
    result: dict[str, Any] | None,
) -> CheckResult:
    check_name = "sfdc.user.assignment_verified"
    pairs = _extract_assignment_pairs(step, result)
    if not pairs:
        return _fail(check_name, "missing assignment results")
    if step.tool == "sfdc.user.assign_permission_sets":
        sobject = "PermissionSetAssignment"
        field_name = "PermissionSetId"
    elif step.tool == "sfdc.user.assign_permission_set_groups":
        sobject = "PermissionSetGroupAssignment"
        field_name = "PermissionSetGroupId"
    else:
        return _skip(check_name, f"unsupported tool {step.tool}")

    query = (
        f"SELECT AssigneeId, {field_name} "
        f"FROM {sobject} "
        f"WHERE AssigneeId IN ({_in_clause(_dedupe([user_id for user_id, _target in pairs]))}) "
        f"AND {field_name} IN ({_in_clause(_dedupe([target for _user_id, target in pairs]))})"
    )
    rows = _run_soql(query, org_alias=_resolve_org_alias(step))
    verified = {
        (str(row.get("AssigneeId")), str(row.get(field_name)))
        for row in rows
        if row.get("AssigneeId") and row.get(field_name)
    }
    missing = [f"{user_id}:{target}" for user_id, target in pairs if (user_id, target) not in verified]
    if missing:
        return _fail(check_name, f"assignment rows not found: {missing}")
    return _pass(check_name, f"verified {len(pairs)} assignment row(s)")


def _check_sfdc_group_membership_verified(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
    result: dict[str, Any] | None,
) -> CheckResult:
    check_name = "sfdc.group.membership_verified"
    pairs = _extract_group_membership_pairs(step, result)
    if not pairs:
        return _fail(check_name, "missing membership results")
    query = (
        "SELECT GroupId, UserOrGroupId "
        "FROM GroupMember "
        f"WHERE GroupId IN ({_in_clause(_dedupe([group_id for _user_id, group_id in pairs]))}) "
        f"AND UserOrGroupId IN ({_in_clause(_dedupe([user_id for user_id, _group_id in pairs]))})"
    )
    rows = _run_soql(query, org_alias=_resolve_org_alias(step))
    verified = {
        (str(row.get("UserOrGroupId")), str(row.get("GroupId")))
        for row in rows
        if row.get("UserOrGroupId") and row.get("GroupId")
    }
    missing = [f"{user_id}:{group_id}" for user_id, group_id in pairs if (user_id, group_id) not in verified]
    if missing:
        return _fail(check_name, f"group memberships not found: {missing}")
    return _pass(check_name, f"verified {len(pairs)} group membership row(s)")


def _check_sfdc_user_deactivated(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
    result: dict[str, Any] | None,
) -> CheckResult:
    check_name = "sfdc.user.deactivated"
    after = result.get("after") if isinstance(result, dict) else {}
    entries = after.get("deactivated") if isinstance(after, dict) else None
    user_ids = _extract_user_ids(step)
    if isinstance(entries, list):
        derived = []
        for entry in entries:
            if not isinstance(entry, dict):
                continue
            derived.extend(_as_list(entry.get("user_id")))
        if derived:
            user_ids = _dedupe(derived)
    if not user_ids:
        return _skip(check_name, "missing user ids")
    rows = _query_by_ids(
        sobject="User",
        fields=["IsActive"],
        ids=user_ids,
        org_alias=_resolve_org_alias(step),
    )
    by_id = {str(row.get("Id")): row for row in rows if row.get("Id")}
    missing = [user_id for user_id in user_ids if user_id not in by_id]
    if missing:
        return _fail(check_name, f"missing users in postcheck: {missing}")
    still_active = [
        user_id
        for user_id, row in by_id.items()
        if str(row.get("IsActive")).strip().lower() in {"true", "1"}
    ]
    if still_active:
        return _fail(check_name, f"users still active: {still_active}")
    return _pass(check_name, f"verified {len(user_ids)} inactive user(s)")


def _check_sfdc_record_owner_updated(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
    result: dict[str, Any] | None,
) -> CheckResult:
    check_name = "sfdc.record.owner_updated"
    sobject = _resolve_sobject(step)
    if not sobject:
        return _skip(check_name, "missing sobject context")
    owner_id = _extract_new_owner_id(step)
    if not owner_id:
        return _skip(check_name, "missing new owner id")
    record_ids = _extract_record_ids(step, result=result)
    if not record_ids:
        return _skip(check_name, "missing record ids")
    rows = _query_by_ids(
        sobject=sobject,
        fields=["OwnerId"],
        ids=record_ids,
        org_alias=_resolve_org_alias(step),
    )
    by_id = {str(row.get("Id")): row for row in rows if row.get("Id")}
    missing = [record_id for record_id in record_ids if record_id not in by_id]
    if missing:
        return _fail(check_name, f"missing records in owner postcheck: {missing}")
    mismatched = [
        record_id
        for record_id, row in by_id.items()
        if str(row.get("OwnerId")) != str(owner_id)
    ]
    if mismatched:
        return _fail(check_name, f"owner mismatch: {mismatched}")
    return _pass(check_name, f"verified owner for {len(record_ids)} record(s)")


def _check_sfdc_forecast_hierarchy_verified(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
    result: dict[str, Any] | None,
) -> CheckResult:
    check_name = "sfdc.forecast.hierarchy_verified"
    pairs = _extract_assignment_pairs(step, result)
    if not pairs:
        return _skip(check_name, "missing role assignment results")
    role_field = str(step.args.get("role_field") or "ForecastingRoleId")
    user_ids = _dedupe([user_id for user_id, _role_id in pairs])
    rows = _query_by_ids(
        sobject="User",
        fields=[role_field],
        ids=user_ids,
        org_alias=_resolve_org_alias(step),
    )
    by_id = {str(row.get("Id")): row for row in rows if row.get("Id")}
    missing_users = [user_id for user_id in user_ids if user_id not in by_id]
    if missing_users:
        return _fail(check_name, f"missing users in role postcheck: {missing_users}")
    mismatches = []
    for user_id, role_id in pairs:
        actual = _get_row_value(by_id.get(user_id, {}), role_field)
        if str(actual) != str(role_id):
            mismatches.append(f"{user_id}:{actual!r}!={role_id!r}")
    if mismatches:
        return _fail(check_name, f"forecast role mismatch: {mismatches}")
    return _pass(check_name, f"verified {len(pairs)} forecast assignment(s)")


def _check_tasks_created(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
    result: dict[str, Any] | None,
) -> CheckResult:
    check_name = "tasks.created"
    entries = _extract_task_entries(result)
    if not entries:
        return _fail(check_name, "missing created task results")
    task_ids = _extract_task_ids(entries)
    if not task_ids:
        return _pass(check_name, f"verified {len(entries)} task result(s) structurally")
    rows = _query_by_ids(
        sobject="Task",
        fields=[],
        ids=task_ids,
        org_alias=_resolve_org_alias(step),
    )
    found = {str(row.get("Id")) for row in rows if row.get("Id")}
    missing = [task_id for task_id in task_ids if task_id not in found]
    if missing:
        return _fail(check_name, f"missing created tasks: {missing}")
    return _pass(check_name, f"verified {len(found)} created task(s)")


def _check_sequence_pause_applied(
    step: PlanStep,
    _tool_def: AutopilotToolDefinition | None,
    result: dict[str, Any] | None,
) -> CheckResult:
    check_name = "sequence.pause.applied"
    after = result.get("after") if isinstance(result, dict) else {}
    if not isinstance(after, dict):
        return _fail(check_name, "missing sequence pause result")
    rows = after.get("records")
    if not isinstance(rows, list) or not rows:
        if after.get("failed"):
            return _fail(check_name, "sequence pause reported failures")
        return _pass(check_name, "no new sequence pauses required")

    failures: list[str] = []
    org_alias = _resolve_org_alias(step)
    for row in rows:
        if not isinstance(row, dict):
            failures.append("invalid row payload")
            continue
        record_id = str(row.get("record_id") or "").strip()
        sobject = str(row.get("sobject") or step.args.get("sobject") or "").strip()
        status = str(row.get("sequence_status") or "").strip().lower()
        if status not in {"paused", "pause_requested"}:
            failures.append(f"{record_id}:unexpected_status={status!r}")
            continue

        paused_states = row.get("paused_sequence_states")
        tag_written = bool(row.get("tag_written"))
        if not tag_written and not paused_states:
            failures.append(f"{record_id}:no tag or paused state evidence")
            continue

        if tag_written:
            tag_field = str(row.get("tag_field") or "").strip()
            tag_value = str(row.get("tag_value") or "").strip()
            if not record_id or not sobject or not tag_field or not tag_value:
                failures.append(f"{record_id or 'unknown'}:missing tag verification context")
                continue
            try:
                _ensure_valid_soql_identifiers(sobject=sobject, fields=[tag_field])
            except InvalidSoqlIdentifierError as exc:
                failures.append(f"{record_id}:invalid soql identifier: {exc}")
                continue
            query = (
                f"SELECT {tag_field} "
                f"FROM {sobject} "
                f"WHERE Id = '{_escape_soql(record_id)}'"
            )
            records = _run_soql(query, org_alias=org_alias)
            if not records:
                failures.append(f"{record_id}:missing CRM record for tag verification")
                continue
            value = _get_row_value(records[0], tag_field)
            if not _contains_pause_tag(value, tag_value):
                failures.append(f"{record_id}:pause tag not persisted")
    if failures:
        return _fail(check_name, "; ".join(failures[:5]))
    return _pass(check_name, f"verified {len(rows)} paused sequence record(s)")


_PRECHECKS: dict[str, PrecheckHandler] = {
    "sfdc.record.exists": _check_sfdc_record_exists,
    "sfdc.user.exists": _check_sfdc_user_exists,
    "sfdc.user.not_already_assigned": _check_sfdc_user_not_already_assigned,
    "sfdc.group.exists": _check_sfdc_group_exists,
    "sfdc.user.not_system_admin": _check_sfdc_user_not_system_admin,
    "sfdc.owner.exists_and_active": _check_sfdc_owner_exists_and_active,
    "sfdc.role.exists": _check_sfdc_role_exists,
}


_POSTCHECKS: dict[str, PostcheckHandler] = {
    "sfdc.record.updated": _check_sfdc_record_updated,
    "sfdc.user.assignment_verified": _check_sfdc_user_assignment_verified,
    "sfdc.group.membership_verified": _check_sfdc_group_membership_verified,
    "sfdc.user.deactivated": _check_sfdc_user_deactivated,
    "sfdc.record.owner_updated": _check_sfdc_record_owner_updated,
    "sfdc.forecast.hierarchy_verified": _check_sfdc_forecast_hierarchy_verified,
    "tasks.created": _check_tasks_created,
    "sequence.pause.applied": _check_sequence_pause_applied,
}


def run_verification(
    plan: PlanV1,
    registry: ToolRegistry,
    *,
    policy: AutonomyPolicy | None = None,
    state_dir: Path | None = None,
    config_path: Path | None = None,
) -> dict[str, str]:
    del registry
    if policy and not policy.defaults.verify_after_execute:
        return {}

    scout_names = list(plan.verification or [])
    if not scout_names:
        return {}

    scan_cfg = load_scan_config(config_path)
    crm = (scan_cfg.crm or "salesforce").lower()
    org_alias = policy.org_alias if policy else scan_cfg.org_alias

    engine = ScoutEngine(user_credentials=_get_user_credentials(), state_dir=state_dir)
    results: dict[str, str] = {}
    for scout_name in scout_names:
        try:
            params = (scan_cfg.per_scout_params or {}).get(scout_name, {})
            finding = engine.run_scout(
                scout_name,
                params=params,
                top_n=scan_cfg.top_n,
                org_alias=org_alias,
                crm=crm,
                hubspot_profile=scan_cfg.hubspot_profile,
                dynamics_profile=scan_cfg.dynamics_profile,
            )
            results[scout_name] = _verification_status(finding)
        except Exception:
            results[scout_name] = "fail"
    return results


def _verification_status(finding: ScoutFinding) -> str:
    if finding.status == "unsupported":
        return "partial"
    if finding.status == "error":
        return "fail"
    if finding.findings_count > 0 or finding.status == "findings":
        return "fail"
    return "pass"


def _get_user_credentials() -> set[str]:
    try:
        from g_gremlin.credentials.provider import CredentialProvider

        provider = CredentialProvider()
        return set(provider.list_configured_integrations())
    except Exception:
        return set()


def _assert_known_checks() -> None:
    declared = set(_PRECHECKS) | set(_POSTCHECKS)
    missing = sorted(_KNOWN_CHECK_NAMES - declared)
    if missing:
        raise RuntimeError(f"Verification handlers missing for checks: {missing}")


_assert_known_checks()


__all__ = [
    "CheckResult",
    "run_prechecks",
    "run_postchecks",
    "run_verification",
]
