"""Declarative form conditions. Not an expression engine or workflow graph.

Field visibility/requiredness run before 7C. Action eligibility runs after
acceptance from canonical values only. No provider/runtime state.
"""
from __future__ import annotations

from collections.abc import Mapping
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, StrictStr, model_validator

from src.domain.forms.canonical import (
    LOGICAL_KEY_PATTERN,
    STRING_FIELD_TYPES,
    FormDefinition,
    FormField,
    FormScalar,
)
from src.domain.forms.validation import (
    FormFieldError,
    classify_input,
    normalize_present_field_value,
)

ConditionOperator = Literal["equals", "not_equals", "is_present", "is_missing"]
FieldConditionEffect = Literal["show_when", "require_when"]

CONDITION_SOURCE_TYPES: frozenset[str] = frozenset(
    {
        "text",
        "email",
        "phone",
        "number",
        "textarea",
        "select",
        "radio",
        "checkbox",
        "date",
        "hidden",
    }
)
CONDITION_OPERATORS: frozenset[str] = frozenset(
    {"equals", "not_equals", "is_present", "is_missing"}
)
HIDDEN_FIELD_POLICY = "reject"
CONDITION_NOT_MET = "condition_not_met"
CONDITIONAL_FIELD_NOT_ALLOWED = "conditional_field_not_allowed"
CONDITIONAL_RULE_INVALID = "conditional_rule_invalid"

FORMS_CONDITIONAL_LOGIC: dict[str, Any] = {
    "slice_id": "7l_conditional_logic",
    "status": "frozen",
    "expression_language": None,
    "operators": tuple(sorted(CONDITION_OPERATORS)),
    "coercion": False,
    "missing_vs_null": "distinct",
    "truthiness": False,
    "hidden_field_policy": HIDDEN_FIELD_POLICY,
    "visibility_default": True,
    "requiredness": "base_or_require_when",
    "required_relaxation": False,
    "file_sources": False,
    "multi_select_sources": False,
    "nested_boolean_ast": False,
    "cross_action_dependencies": False,
    "routing_mutation": False,
    "computed_values": False,
    "one_pass_evaluation": True,
    "duplicate_target_effect": "reject",
    "self_dependency": "reject",
    "cycles": "reject",
    "action_ineligible": CONDITION_NOT_MET,
    "server_authoritative": True,
    "public_bff": False,
    "form_submit_capability": False,
    "codegen": False,
    "app_block_migrated": False,
    "schema_migration": False,
    "docs": "docs/audits/FORMS_CONDITIONAL_LOGIC_7L.md",
}


class FormConditionError(ValueError):
    def __init__(self, code: str = CONDITIONAL_RULE_INVALID) -> None:
        self.code = code
        super().__init__(code)


class FormCondition(BaseModel):
    """One typed predicate over a scalar source field."""

    model_config = ConfigDict(extra="forbid", frozen=True)
    field_key: StrictStr = Field(min_length=1, max_length=64, pattern=LOGICAL_KEY_PATTERN)
    operator: ConditionOperator
    value: FormScalar = None

    @model_validator(mode="after")
    def presence_operators_have_no_value(self):
        if self.operator in {"is_present", "is_missing"} and "value" in self.model_fields_set:
            raise FormConditionError(CONDITIONAL_RULE_INVALID)
        return self


class FieldConditionalRule(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    target_field: StrictStr = Field(
        min_length=1, max_length=64, pattern=LOGICAL_KEY_PATTERN
    )
    effect: FieldConditionEffect
    condition: FormCondition


class ActionConditionalRule(BaseModel):
    """run_when only. Never mutates routing or payload."""

    model_config = ConfigDict(extra="forbid", frozen=True)
    action_key: StrictStr = Field(min_length=1, max_length=64, pattern=LOGICAL_KEY_PATTERN)
    condition: FormCondition


class FormConditionSet(BaseModel):
    """Separate from FormDefinition. Empty set is unconditional."""

    model_config = ConfigDict(extra="forbid", frozen=True)
    field_rules: tuple[FieldConditionalRule, ...] = ()
    action_rules: tuple[ActionConditionalRule, ...] = ()


class FieldConditionalState(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    visible_fields: tuple[str, ...]
    required_fields: tuple[str, ...]


class ActionEligibilityState(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    eligible_actions: tuple[str, ...]
    ineligible_actions: tuple[str, ...]


def _is_number(value: Any) -> bool:
    return isinstance(value, (int, float)) and not isinstance(value, bool)


def _strict_equal(left: Any, right: Any) -> bool:
    if left is None and right is None:
        return True
    if isinstance(left, bool) or isinstance(right, bool):
        return isinstance(left, bool) and isinstance(right, bool) and left is right
    if _is_number(left) and _is_number(right):
        return left == right
    return type(left) is type(right) and left == right


def _comparison_compatible(field: FormField, value: Any) -> bool:
    if value is None:
        return True
    if field.type == "checkbox":
        return isinstance(value, bool)
    if field.type == "number":
        return _is_number(value)
    if field.type in STRING_FIELD_TYPES:
        return isinstance(value, str)
    return False


def _field_map(definition: FormDefinition) -> dict[str, FormField]:
    return {field.key: field for field in definition.fields}


class _SourceState:
    __slots__ = ("kind", "value")

    def __init__(self, kind: Literal["missing", "null", "value"], value: Any = None) -> None:
        self.kind = kind
        self.value = value


def _snapshot_raw_field(field: FormField, raw: Mapping[str, Any]) -> _SourceState:
    if field.key not in raw:
        return _SourceState("missing")
    classified = classify_input(raw, field.key)
    if classified == "null":
        return _SourceState("null")
    if classified == "blank" and field.type in STRING_FIELD_TYPES:
        return _SourceState("missing")
    parsed, err = normalize_present_field_value(field, raw[field.key])
    if err == "blank":
        return _SourceState("missing")
    if err:
        return _SourceState("value", raw[field.key])
    return _SourceState("value", parsed)


def _snapshot_accepted_field(values: Mapping[str, Any], key: str) -> _SourceState:
    if key not in values:
        return _SourceState("missing")
    value = values[key]
    if value is None:
        return _SourceState("null")
    return _SourceState("value", value)


def _condition_matches(condition: FormCondition, state: _SourceState) -> bool:
    if condition.operator == "is_missing":
        return state.kind == "missing"
    if condition.operator == "is_present":
        return state.kind != "missing"
    if condition.operator == "equals":
        if state.kind == "missing":
            return False
        if state.kind == "null":
            return condition.value is None
        return _strict_equal(state.value, condition.value)
    if condition.operator == "not_equals":
        if state.kind == "missing":
            return False
        if state.kind == "null":
            return condition.value is not None
        return not _strict_equal(state.value, condition.value)
    return False


def _has_cycle(edges: dict[str, set[str]]) -> bool:
    visiting: set[str] = set()
    visited: set[str] = set()

    def walk(node: str) -> bool:
        if node in visiting:
            return True
        if node in visited:
            return False
        visiting.add(node)
        for nxt in edges.get(node, ()):
            if walk(nxt):
                return True
        visiting.remove(node)
        visited.add(node)
        return False

    return any(walk(node) for node in edges)


def validate_condition_rules(
    rules: FormConditionSet,
    definition: FormDefinition,
    *,
    action_keys: tuple[str, ...] | None = None,
) -> tuple[str, ...]:
    """Configuration-time checks. Empty tuple means the rule set is usable."""
    errors: list[str] = []
    fields = _field_map(definition)
    seen_field_effects: set[tuple[str, str]] = set()
    seen_actions: set[str] = set()
    graph: dict[str, set[str]] = {}

    def check_condition(condition: FormCondition) -> None:
        source = fields.get(condition.field_key)
        if source is None:
            errors.append(CONDITIONAL_RULE_INVALID)
            return
        if source.type not in CONDITION_SOURCE_TYPES:
            errors.append(CONDITIONAL_RULE_INVALID)
            return
        if condition.operator not in CONDITION_OPERATORS:
            errors.append(CONDITIONAL_RULE_INVALID)
            return
        if condition.operator in {"equals", "not_equals"}:
            if not _comparison_compatible(source, condition.value):
                errors.append(CONDITIONAL_RULE_INVALID)
                return
            if (
                source.type in {"select", "radio"}
                and isinstance(condition.value, str)
                and source.options
                and condition.value not in {option.value for option in source.options}
            ):
                errors.append(CONDITIONAL_RULE_INVALID)

    for rule in rules.field_rules:
        target = fields.get(rule.target_field)
        if target is None:
            errors.append(CONDITIONAL_RULE_INVALID)
            continue
        key = (rule.target_field, rule.effect)
        if key in seen_field_effects:
            errors.append(CONDITIONAL_RULE_INVALID)
            continue
        seen_field_effects.add(key)
        if rule.target_field == rule.condition.field_key:
            errors.append(CONDITIONAL_RULE_INVALID)
            continue
        check_condition(rule.condition)
        graph.setdefault(rule.condition.field_key, set()).add(rule.target_field)

    if _has_cycle(graph):
        errors.append(CONDITIONAL_RULE_INVALID)

    for rule in rules.action_rules:
        if rule.action_key in seen_actions:
            errors.append(CONDITIONAL_RULE_INVALID)
            continue
        seen_actions.add(rule.action_key)
        if action_keys is not None and rule.action_key not in action_keys:
            errors.append(CONDITIONAL_RULE_INVALID)
            continue
        check_condition(rule.condition)

    return tuple(dict.fromkeys(errors))


def evaluate_field_conditions(
    definition: FormDefinition,
    raw: Mapping[str, Any],
    rules: FormConditionSet,
) -> FieldConditionalState:
    """One-pass visibility/requiredness from a candidate snapshot. Pure."""
    config = validate_condition_rules(rules, definition)
    if config:
        raise FormConditionError(config[0])
    fields = _field_map(definition)
    snapshot = {
        key: _snapshot_raw_field(field, raw) for key, field in fields.items()
        if field.type in CONDITION_SOURCE_TYPES
    }
    show_rules = {
        rule.target_field: rule
        for rule in rules.field_rules
        if rule.effect == "show_when"
    }
    require_rules = {
        rule.target_field: rule
        for rule in rules.field_rules
        if rule.effect == "require_when"
    }
    visible: list[str] = []
    required: list[str] = []
    for field in definition.fields:
        show = show_rules.get(field.key)
        is_visible = True
        if show is not None:
            source = snapshot.get(show.condition.field_key)
            is_visible = source is not None and _condition_matches(show.condition, source)
        if is_visible:
            visible.append(field.key)
            if field.required:
                required.append(field.key)
            else:
                req = require_rules.get(field.key)
                if req is not None:
                    source = snapshot.get(req.condition.field_key)
                    if source is not None and _condition_matches(req.condition, source):
                        required.append(field.key)
    return FieldConditionalState(
        visible_fields=tuple(visible),
        required_fields=tuple(required),
    )


def evaluate_action_conditions(
    values: Mapping[str, Any],
    rules: FormConditionSet,
    *,
    action_keys: tuple[str, ...],
) -> ActionEligibilityState:
    """Eligibility from accepted canonical values only. No action outputs."""
    run_when = {rule.action_key: rule for rule in rules.action_rules}
    eligible: list[str] = []
    ineligible: list[str] = []
    for key in action_keys:
        rule = run_when.get(key)
        if rule is None:
            eligible.append(key)
            continue
        state = _snapshot_accepted_field(values, rule.condition.field_key)
        if _condition_matches(rule.condition, state):
            eligible.append(key)
        else:
            ineligible.append(key)
    return ActionEligibilityState(
        eligible_actions=tuple(eligible),
        ineligible_actions=tuple(ineligible),
    )


def _conditional_field_error(field: str) -> FormFieldError:
    return FormFieldError(
        field=field,
        code=CONDITIONAL_FIELD_NOT_ALLOWED,
        message="This field cannot be submitted in the current form state.",
    )


def effective_definition_for_state(
    definition: FormDefinition,
    state: FieldConditionalState,
) -> FormDefinition:
    visible = set(state.visible_fields)
    required = set(state.required_fields)
    fields: list[FormField] = []
    for field in definition.fields:
        if field.key not in visible:
            continue
        if field.key in required and not field.required:
            fields.append(field.model_copy(update={"required": True}))
        else:
            fields.append(field)
    if not fields:
        raise FormConditionError(CONDITIONAL_RULE_INVALID)
    return FormDefinition(
        key=definition.key,
        title=definition.title,
        description=definition.description,
        fields=fields,
    )


def hidden_submitted_field_errors(
    definition: FormDefinition,
    raw: Mapping[str, Any],
    state: FieldConditionalState,
) -> tuple[FormFieldError, ...]:
    declared = {field.key for field in definition.fields}
    visible = set(state.visible_fields)
    errors = [
        _conditional_field_error(str(key))
        for key in sorted(raw)
        if key in declared and key not in visible
    ]
    return tuple(errors)


def prepare_conditional_validation(
    definition: FormDefinition,
    raw: Mapping[str, Any],
    rules: FormConditionSet | None,
) -> tuple[FormDefinition, Mapping[str, Any], tuple[FormFieldError, ...]]:
    """Derive effective 7C input. Does not validate values."""
    if rules is None or (not rules.field_rules and not rules.action_rules):
        return definition, raw, ()
    field_state = evaluate_field_conditions(definition, raw, rules)
    hidden_errors = hidden_submitted_field_errors(definition, raw, field_state)
    visible = set(field_state.visible_fields)
    declared = {field.key for field in definition.fields}
    stripped = {
        key: value
        for key, value in raw.items()
        if key not in declared or key in visible
    }
    effective = effective_definition_for_state(definition, field_state)
    return effective, stripped, hidden_errors
