"""Validation rules a field can carry — a Form's fields (Form Designer ›
Validation), a datasource's fields (Datasources › field › Validation) and
the registration form's. Checked in the browser (frontend
lib/formValidation.ts) and on the server (app/core/form_validation.py)."""

import re
from typing import Literal

from pydantic import BaseModel, Field, model_validator

from app.core.expr import expression_error
from app.core.form_validation import resolve_date_limit

FormValidationRuleType = Literal[
    "min_length", "max_length", "pattern", "email", "url", "phone",
    "min_value", "max_value",
    "min_date", "max_date",
    "min_selections", "max_selections",
    "equals_field",
    # value is an expression (app/core/expr.py) that must be true; the
    # field's own value is `value`, other fields by name.
    "expression",
    # Checked on the server only, against stored data (app/crud/form_stored_rules.py):
    # "unique" — no other submission of this form holds the same value;
    # "exists_in" — some record of `source` ("master:<id>" / "form:<id>")
    # holds it in `source_field`. Form fields only.
    "unique", "exists_in",
]
SERVER_RULES = {"unique", "exists_in"}
_RULES_WITHOUT_VALUE = {"email", "url", "phone", "unique", "exists_in"}
_NUMERIC_RULES = {"min_length", "max_length", "min_value", "max_value", "min_selections", "max_selections"}
_DATE_RULES = {"min_date", "max_date"}


class FormValidationRule(BaseModel):
    """One of a field's validation rules (Form Designer's "Validation"
    section), checked only when the field has a value — emptiness is
    `required`'s job. Enforced in the browser (frontend lib/
    formValidation.ts) and again on submission (app/core/
    form_validation.py). `value` is the limit: a number for the length/
    value/selection rules, a regex the whole value must match for
    "pattern", YYYY-MM-DD or today/today+N/today-N for the date rules,
    another field's name for "equals_field", unused for email/url/phone.
    `message` replaces the default error text; "{label}"/"{limit}" in it
    are substituted."""

    type: FormValidationRuleType
    value: str | float | None = None
    message: str | None = None
    # "exists_in" only — where the value must be found.
    source: str | None = Field(default=None, pattern=r"^(master|form):[0-9a-fA-F-]{36}$")
    source_field: str | None = None
    # "unique" only — unique among submissions with the same answers to
    # these (top-level) fields, e.g. ["program"]; blank = across the form.
    scope_fields: list[str] = []

    @model_validator(mode="after")
    def _check_value(self) -> "FormValidationRule":
        if self.type == "exists_in":
            if not self.source or not (self.source_field or "").strip():
                raise ValueError("validation rule 'exists_in' needs the datasource or form and its field to look in")
        elif self.source is not None or self.source_field is not None:
            raise ValueError("source/source_field are only meaningful for the 'exists_in' rule")
        if self.scope_fields and self.type != "unique":
            raise ValueError("scope_fields is only meaningful for the 'unique' rule")
        if self.type in _RULES_WITHOUT_VALUE:
            return self
        raw = "" if self.value is None else str(self.value).strip()
        if not raw:
            raise ValueError(f"validation rule '{self.type}' needs a value")
        if self.type in _NUMERIC_RULES:
            try:
                number = float(raw)
            except ValueError:
                raise ValueError(f"validation rule '{self.type}' needs a numeric value") from None
            if self.type not in ("min_value", "max_value") and number < 0:
                raise ValueError(f"validation rule '{self.type}' can't be negative")
        elif self.type == "pattern":
            try:
                re.compile(raw)
            except re.error as exc:
                raise ValueError(f"validation rule 'pattern' isn't a valid regular expression: {exc}") from None
        elif self.type == "expression":
            error = expression_error(raw)
            if error:
                raise ValueError(f"validation rule 'expression': {error}")
        elif self.type in _DATE_RULES and resolve_date_limit(raw) is None:
            raise ValueError(f"validation rule '{self.type}' needs YYYY-MM-DD, today, today+N or today-N")
        return self
