"""Sheriff Validator - Deterministic safety validation for FlowPlan.

Sheriff is the ONLY deployment blocker. All checks are deterministic.
"""

from __future__ import annotations

import logging
from typing import List, Optional, Set

from ..api.v2.flow_builder_schemas import (
    FieldMetadata,
    FlowAction,
    FlowCondition,
    FlowPlan,
    FlowTriggerType,
    SalesforceFieldType,
    ScoutContext,
    TriggerEvent,
    ValidationIssue,
    ValidationResult,
    ValidationSeverity,
)
from .flow_async_fields import collect_condition_fields, get_async_field_category

log = logging.getLogger(__name__)

FORBIDDEN_FIELDS = {
    "Id",
    "CreatedById",
    "CreatedDate",
    "LastModifiedById",
    "LastModifiedDate",
    "SystemModstamp",
    "IsDeleted",
}

HIGH_RISK_FIELDS = {"RecordTypeId", "OwnerId", "ParentId", "MasterRecordId"}


class FlowSheriff:
    """Deterministic validator - no AI, pure rules. ONLY deployment blocker."""

    def __init__(self, context: ScoutContext):
        self.context = context
        self.issues: List[ValidationIssue] = []

    def validate(self, plan: FlowPlan) -> ValidationResult:
        """Run all validation checks. Returns deployable=False if any ERROR."""
        self.issues = []

        # v1 scope checks (hard errors)
        self._check_v1_scope(plan)
        self._check_namespace_isolation(plan)
        self._check_trigger_object_match(plan)
        self._check_branch_conditions(plan)

        # Field checks
        self._check_field_existence(plan)
        self._check_field_case_mismatch(plan)
        self._check_async_fields_before_save(plan)
        self._check_type_safety(plan)
        self._check_forbidden_fields(plan)
        self._check_action_updateability(plan)

        # Picklist validation (requires record type)
        self._check_picklist_values(plan)

        # Record type scope: if using RT-specific picklists, require RT condition
        self._check_record_type_scope(plan)

        # Safety guards (blind update is ERROR in v1)
        self._check_blind_update_guard(plan)

        # Risk warnings (non-blocking)
        self._check_high_risk_fields(plan)
        self._check_infinite_loop_risk(plan)
        self._check_branch_conflicts(plan)

        return ValidationResult(
            valid=not any(i.severity == ValidationSeverity.ERROR for i in self.issues),
            deployable=not any(i.severity == ValidationSeverity.ERROR for i in self.issues),
            issues=self.issues,
        )

    def _add_error(
        self,
        code: str,
        message: str,
        field: Optional[str] = None,
        suggestion: Optional[str] = None,
        branch_name: Optional[str] = None,
        branch_index: Optional[int] = None,
    ):
        self.issues.append(
            ValidationIssue(
                severity=ValidationSeverity.ERROR,
                code=code,
                message=message,
                field=field,
                suggestion=suggestion,
                branch_name=branch_name,
                branch_index=branch_index,
            )
        )

    def _add_warning(
        self,
        code: str,
        message: str,
        field: Optional[str] = None,
        suggestion: Optional[str] = None,
        branch_name: Optional[str] = None,
        branch_index: Optional[int] = None,
    ):
        self.issues.append(
            ValidationIssue(
                severity=ValidationSeverity.WARNING,
                code=code,
                message=message,
                field=field,
                suggestion=suggestion,
                branch_name=branch_name,
                branch_index=branch_index,
            )
        )

    @staticmethod
    def _iter_branch_conditions_with_scope(plan: FlowPlan):
        for idx, branch in enumerate(plan.branches, start=1):
            for condition in branch.conditions:
                yield branch, idx, condition

    @staticmethod
    def _iter_branch_actions_with_scope(plan: FlowPlan):
        for idx, branch in enumerate(plan.branches, start=1):
            for action in branch.actions:
                yield branch, idx, action

    @staticmethod
    def _iter_top_level_actions(plan: FlowPlan):
        for action in plan.actions:
            yield action

    @staticmethod
    def _iter_entry_conditions_with_scope(plan: FlowPlan):
        if plan.entry_criteria and plan.entry_criteria.conditions:
            for condition in plan.entry_criteria.conditions:
                yield None, None, condition

    @staticmethod
    def _iter_entry_actions_with_scope(plan: FlowPlan):
        for action in plan.actions:
            yield None, None, action

    @staticmethod
    def _iter_conditions_with_scope(plan: FlowPlan):
        for branch_name, branch_index, condition in FlowSheriff._iter_entry_conditions_with_scope(plan):
            yield branch_name, branch_index, condition
        for branch, branch_index, condition in FlowSheriff._iter_branch_conditions_with_scope(plan):
            yield branch.name, branch_index, condition

    @staticmethod
    def _iter_actions_with_scope(plan: FlowPlan):
        for branch_name, branch_index, action in FlowSheriff._iter_entry_actions_with_scope(plan):
            yield branch_name, branch_index, action
        for branch, branch_index, action in FlowSheriff._iter_branch_actions_with_scope(plan):
            yield branch.name, branch_index, action

    def _check_v1_scope(self, plan: FlowPlan):
        """Enforce v1 constraints."""
        if plan.trigger.type not in (
            FlowTriggerType.RECORD_BEFORE_SAVE,
            FlowTriggerType.RECORD_AFTER_SAVE,
        ):
            self._add_error(
                "V1_SCOPE_VIOLATION",
                f"Unsupported trigger type {plan.trigger.type}",
                suggestion="Use RecordBeforeSave or RecordAfterSave",
            )
        elif plan.trigger.type == FlowTriggerType.RECORD_AFTER_SAVE:
            self._add_warning(
                "AFTER_SAVE_FLOW",
                "RecordAfterSave flows can re-trigger on updates. Ensure entry criteria prevents recursion.",
                suggestion="Add conditions like Status != '<value>' to avoid loops",
            )

        if plan.entry_criteria and plan.entry_criteria.logic not in ("AND", "OR"):
            self._add_error(
                "V1_SCOPE_VIOLATION",
                f"v1 only supports AND/OR logic, got {plan.entry_criteria.logic}",
            )

    def _check_branch_conditions(self, plan: FlowPlan) -> None:
        """Require each branch to have at least one condition."""
        if not plan.branches:
            return

        for idx, branch in enumerate(plan.branches, start=1):
            if not branch.conditions:
                self._add_error(
                    "BRANCH_NO_CONDITIONS",
                    f"Branch '{branch.name}' must include at least one condition.",
                    suggestion="Add branch conditions or move actions to top-level actions.",
                    branch_name=branch.name,
                    branch_index=idx,
                )

        if plan.branches:
            for branch, branch_index, action in self._iter_branch_actions_with_scope(plan):
                if action.type != "assignment":
                    self._add_error(
                        "V1_SCOPE_VIOLATION",
                        f"v1 only supports assignment actions, got {action.type}",
                        branch_name=branch.name,
                        branch_index=branch_index,
                    )
        else:
            for action in self._iter_top_level_actions(plan):
                if action.type != "assignment":
                    self._add_error(
                        "V1_SCOPE_VIOLATION",
                        f"v1 only supports assignment actions, got {action.type}",
                    )

        if plan.branches:
            for idx, branch in enumerate(plan.branches, start=1):
                if branch.logic not in ("AND", "OR"):
                    self._add_error(
                        "V1_SCOPE_VIOLATION",
                        f"v1 only supports AND/OR logic, got {branch.logic}",
                        branch_name=branch.name,
                        branch_index=idx,
                    )

    def _check_namespace_isolation(self, plan: FlowPlan):
        if not plan.flow_name.startswith("FO_"):
            self._add_error(
                "NAMESPACE_VIOLATION",
                f"Flow name '{plan.flow_name}' must start with 'FO_' prefix",
                suggestion=f"Rename to 'FO_{plan.flow_name}'",
            )

    def _check_trigger_object_match(self, plan: FlowPlan):
        """Ensure trigger object matches ScoutContext."""
        if plan.trigger.object_api_name != self.context.object_api_name:
            self._add_error(
                "OBJECT_MISMATCH",
                f"Trigger object '{plan.trigger.object_api_name}' does not match context '{self.context.object_api_name}'",
            )

    def _check_field_existence(self, plan: FlowPlan):
        referenced: Set[tuple] = set()

        def _check(field_name, branch_name=None, branch_index=None):
            if not field_name:
                return
            key = (field_name, branch_name, branch_index)
            if key in referenced:
                return
            referenced.add(key)

            if not self.context.field_exists(field_name):
                self._add_error(
                    "FIELD_NOT_FOUND",
                    f"Field '{field_name}' does not exist on {self.context.object_api_name}",
                    field=field_name,
                    branch_name=branch_name,
                    branch_index=branch_index,
                )

        for branch_name, branch_index, condition in self._iter_conditions_with_scope(plan):
            _check(condition.field, branch_name, branch_index)
            if condition.value_is_field and isinstance(condition.value, str):
                _check(condition.value, branch_name, branch_index)

        for branch_name, branch_index, action in self._iter_actions_with_scope(plan):
            _check(action.field, branch_name, branch_index)
            if action.value.source == "field_reference" and isinstance(action.value.content, str):
                _check(action.value.content, branch_name, branch_index)

    def _check_field_case_mismatch(self, plan: FlowPlan):
        """Error on case mismatch with suggestion for canonical name."""

        def check_field(field_name: str, branch_name=None, branch_index=None):
            canonical = self.context.get_canonical_name(field_name)
            if canonical and canonical != field_name:
                self._add_error(
                    "FIELD_CASE_MISMATCH",
                    f"Field '{field_name}' has incorrect case",
                    field=field_name,
                    suggestion=f"Use canonical name: {canonical}",
                    branch_name=branch_name,
                    branch_index=branch_index,
                )

        for branch_name, branch_index, condition in self._iter_conditions_with_scope(plan):
            check_field(condition.field, branch_name, branch_index)
        for branch_name, branch_index, action in self._iter_actions_with_scope(plan):
            check_field(action.field, branch_name, branch_index)

    def _check_async_fields_before_save(self, plan: FlowPlan) -> None:
        """Block async-populated fields (EAC, Einstein Scoring) in RecordBeforeSave conditions.

        These fields are populated asynchronously by Salesforce Einstein features and are
        NOT available in before-save triggers. Common patterns:
        - EAC_* : Einstein Activity Capture (email/calendar sync)
        - Lead_Score__c, LeadScore* : Einstein Lead Scoring
        - Opportunity_Score__c, OpportunityScore* : Einstein Opportunity Scoring
        """
        if plan.trigger.type != FlowTriggerType.RECORD_BEFORE_SAVE:
            return

        fields: Set[str] = set()
        if plan.entry_criteria:
            fields.update(collect_condition_fields(plan.entry_criteria.conditions))
        if plan.branches:
            for branch in plan.branches:
                fields.update(collect_condition_fields(branch.conditions or []))

        # Check for async-populated field patterns
        async_fields: dict[str, str] = {}  # field -> category
        for f in fields:
            category = get_async_field_category(f)
            if category:
                async_fields[f] = category

        if not async_fields:
            return

        # Group by category for cleaner error message
        by_category: dict[str, list[str]] = {}
        for field, category in async_fields.items():
            by_category.setdefault(category, []).append(field)

        for category, cat_fields in by_category.items():
            self._add_error(
                "ASYNC_FIELD_BEFORE_SAVE",
                f"{category} fields ({', '.join(sorted(cat_fields))}) are not available in RecordBeforeSave triggers.",
                field=cat_fields[0],
                suggestion="Use RecordAfterSave trigger type, or replace with non-async fields.",
            )

    def _check_type_safety(self, plan: FlowPlan):
        for branch_name, branch_index, action in self._iter_actions_with_scope(plan):
            field_meta = self.context.get_field(action.field)
            if not field_meta or action.value.source != "literal":
                continue

            value = action.value.content
            ftype = field_meta.type

            if ftype == SalesforceFieldType.BOOLEAN:
                if not isinstance(value, bool) and value not in (
                    True,
                    False,
                    "true",
                    "false",
                ):
                    self._add_error(
                        "TYPE_MISMATCH",
                        f"Field '{action.field}' expects boolean, got '{type(value).__name__}'",
                        field=action.field,
                        branch_name=branch_name,
                        branch_index=branch_index,
                    )
            elif ftype in (
                SalesforceFieldType.DOUBLE,
                SalesforceFieldType.CURRENCY,
                SalesforceFieldType.PERCENT,
            ):
                if value is not None:
                    try:
                        float(value)
                    except (ValueError, TypeError):
                        self._add_error(
                            "TYPE_MISMATCH",
                            f"Field '{action.field}' expects number, got '{value}'",
                            field=action.field,
                            branch_name=branch_name,
                            branch_index=branch_index,
                        )

    def _check_picklist_values(self, plan: FlowPlan):
        """Validate picklist values using record-type-specific restrictions."""
        record_type_id = plan.trigger.record_type_id

        for branch_name, branch_index, action in self._iter_actions_with_scope(plan):
            field_meta = self.context.get_field(action.field)
            if not field_meta or action.value.source != "literal":
                continue
            if field_meta.type not in (
                SalesforceFieldType.PICKLIST,
                SalesforceFieldType.MULTIPICKLIST,
            ):
                continue

            # Get values for this record type
            valid_values = self.context.get_picklist_values(action.field, record_type_id)

            # If picklist_restrictions exist but no record type selected, block
            if action.field in self.context.picklist_restrictions and not record_type_id:
                self._add_error(
                    "RECORD_TYPE_REQUIRED",
                    f"Picklist '{action.field}' has record-type-specific values. Select a record type.",
                    field=action.field,
                    suggestion="Select a record type in the wizard or set trigger.record_type_id",
                    branch_name=branch_name,
                    branch_index=branch_index,
                )
                continue

            if not valid_values:
                self._add_warning(
                    "PICKLIST_UNKNOWN",
                    f"Could not verify picklist values for '{action.field}'",
                    field=action.field,
                    branch_name=branch_name,
                    branch_index=branch_index,
                )
                continue

            value = action.value.content
            values = (
                str(value).split(";")
                if field_meta.type == SalesforceFieldType.MULTIPICKLIST
                else [value]
            )

            for v in values:
                v_stripped = str(v).strip() if v else ""
                if v_stripped and v_stripped not in valid_values:
                    self._add_error(
                        "INVALID_PICKLIST_VALUE",
                        f"'{v_stripped}' is not valid for '{action.field}'. Valid: {valid_values[:5]}{'...' if len(valid_values) > 5 else ''}",
                        field=action.field,
                        suggestion=f"Use: {', '.join(valid_values[:5])}",
                        branch_name=branch_name,
                        branch_index=branch_index,
                    )

    def _check_blind_update_guard(self, plan: FlowPlan):
        """
        BLOCKING ERROR if flow runs on updates without entry criteria.
        This is v1's most important safety guard.
        """
        runs_on_update = plan.runs_on_update()
        has_criteria = plan.has_entry_criteria()

        if runs_on_update and plan.branches:
            for branch, branch_index, _ in self._iter_branch_actions_with_scope(plan):
                if not branch.conditions and not has_criteria:
                    self._add_error(
                        "BLIND_UPDATE",
                        "Flow branch runs on updates without entry criteria or branch conditions.",
                        suggestion="Add branch conditions or entry criteria, or change trigger to Create-only",
                        branch_name=branch.name,
                        branch_index=branch_index,
                    )
            return

        if runs_on_update and not has_criteria:
            self._add_error(
                "BLIND_UPDATE",
                "Flow runs on updates without entry criteria. This will overwrite data on EVERY update.",
                suggestion="Add entry criteria, or change trigger to Create-only",
            )

    def _check_forbidden_fields(self, plan: FlowPlan):
        for branch_name, branch_index, action in self._iter_actions_with_scope(plan):
            if action.field in FORBIDDEN_FIELDS:
                self._add_error(
                    "FORBIDDEN_FIELD",
                    f"Cannot assign to system field '{action.field}'",
                    field=action.field,
                    branch_name=branch_name,
                    branch_index=branch_index,
                )

    def _check_action_updateability(self, plan: FlowPlan) -> None:
        """Ensure action targets are updateable/createable and not formula fields."""
        if not self.context or not self.context.fields:
            return

        runs_on_update = TriggerEvent.UPDATE in plan.trigger.events

        for branch_name, branch_index, action in self._iter_actions_with_scope(plan):
            field_meta = self.context.get_field(action.field)
            if not field_meta:
                continue

            if field_meta.is_formula:
                    self._add_error(
                        "FIELD_NOT_UPDATEABLE",
                        f"Field '{action.field}' is a formula field and cannot be updated.",
                        field=action.field,
                        branch_name=branch_name,
                        branch_index=branch_index,
                    )
                    continue

            if runs_on_update:
                if not field_meta.updateable:
                        self._add_error(
                            "FIELD_NOT_UPDATEABLE",
                            f"Field '{action.field}' is not updateable for this user/profile.",
                            field=action.field,
                            suggestion="Choose a different field or adjust field-level security.",
                            branch_name=branch_name,
                            branch_index=branch_index,
                        )
            else:
                if not (field_meta.createable or field_meta.updateable):
                    self._add_error(
                        "FIELD_NOT_UPDATEABLE",
                        f"Field '{action.field}' is not createable for this user/profile.",
                        field=action.field,
                        suggestion="Choose a different field or adjust field-level security.",
                        branch_name=branch_name,
                        branch_index=branch_index,
                    )

    def _check_high_risk_fields(self, plan: FlowPlan):
        for branch_name, branch_index, action in self._iter_actions_with_scope(plan):
            if action.field in HIGH_RISK_FIELDS:
                self._add_warning(
                    "HIGH_RISK_FIELD",
                    f"Modifying '{action.field}' can have significant side effects",
                    field=action.field,
                    branch_name=branch_name,
                    branch_index=branch_index,
                )

    def _check_infinite_loop_risk(self, plan: FlowPlan):
        """
        Smarter recursion check: warn only if the condition doesn't prevent re-trigger.
        """
        if not plan.runs_on_update():
            return

        entry_conditions = plan.entry_criteria.conditions if plan.entry_criteria else []

        def _has_safe_guard(conditions, action) -> bool:
            for cond in conditions:
                if cond.field != action.field:
                    continue
                if cond.operator.value == "IsNull":
                    return True
                if cond.operator.value == "NotEqualTo" and action.value.source == "literal":
                    if cond.value == action.value.content:
                        return True
            return False

        def _warn(action, branch_name=None, branch_index=None):
            self._add_warning(
                "INFINITE_LOOP_RISK",
                f"Field '{action.field}' is in entry criteria and actions. Ensure condition prevents re-trigger.",
                field=action.field,
                suggestion=f"Add condition: {action.field} != '{action.value.content}'",
                branch_name=branch_name,
                branch_index=branch_index,
            )

        if plan.branches:
            for idx, branch in enumerate(plan.branches, start=1):
                for action in branch.actions:
                    applicable = (branch.conditions or []) + entry_conditions
                    branch_name = branch.name
                    branch_index = idx
                    if not any(c.field == action.field for c in applicable):
                        continue
                    if _has_safe_guard(applicable, action):
                        continue
                    _warn(action, branch_name, branch_index)
            return

        for action in plan.actions:
            if not any(c.field == action.field for c in entry_conditions):
                continue
            if _has_safe_guard(entry_conditions, action):
                continue
            _warn(action, None, None)

    def _check_branch_conflicts(self, plan: FlowPlan):
        """Warn if branches set the same field to different literal values."""
        if not plan.branches:
            return

        field_values: dict[str, set[str]] = {}
        for branch in plan.branches:
            for action in branch.actions:
                if action.value.source != "literal":
                    continue
                val = action.value.content
                field_values.setdefault(action.field, set()).add(str(val))

        for field, values in field_values.items():
            if len(values) > 1:
                self._add_warning(
                    "BRANCH_CONFLICT",
                    f"Branches set '{field}' to multiple values ({', '.join(list(values)[:3])}). Ensure conditions are mutually exclusive or branch order is correct.",
                    field=field,
                )

    def _is_master_record_type(self, record_type_id: str) -> bool:
        """Check if this is the Master record type (default/fallback)."""
        # Master RT ID pattern: 012 + 15 zeros + AAA (e.g., 012000000000000AAA)
        # It's the "no record type" default - doesn't need scoping
        if not record_type_id:
            return False
        return record_type_id.startswith("012") and record_type_id.endswith("AAA")

    def _check_record_type_scope(self, plan: FlowPlan):
        """
        If using record-type-specific picklist values, require RecordTypeId in entry criteria.
        Otherwise the flow can run on other record types and cause "bad value for restricted picklist".
        """
        record_type_id = plan.trigger.record_type_id
        if not record_type_id:
            return  # No RT-specific values, nothing to check

        # Skip check for Master record type - it's the default/fallback
        if self._is_master_record_type(record_type_id):
            return

        # Check if any assigned picklist has RT-specific restrictions
        has_rt_restricted_picklist = False
        for action in plan.iter_actions():
            if action.field in self.context.picklist_restrictions:
                has_rt_restricted_picklist = True
                break

        if not has_rt_restricted_picklist:
            return

        # Check if entry criteria includes RecordTypeId condition
        has_rt_condition = False
        if plan.entry_criteria:
            for c in plan.entry_criteria.conditions:
                if c.field == "RecordTypeId" and c.operator.value == "EqualTo":
                    has_rt_condition = True
                    break
        for branch in plan.branches:
            for c in branch.conditions:
                if c.field == "RecordTypeId" and c.operator.value == "EqualTo":
                    has_rt_condition = True
                    break

        if not has_rt_condition:
            self._add_error(
                "RECORD_TYPE_SCOPE_MISSING",
                f"Picklist values are restricted to record type '{record_type_id}'. Add RecordTypeId condition to entry criteria.",
                suggestion=f"Add condition: RecordTypeId EqualTo '{record_type_id}'",
            )
