"""Validates master-record field_values against a master's field_schema
(FR-MST-005: "data type and required-field checks"). Shared by the
interactive record endpoints and the bulk-import endpoint.

FR-MST-002 requires "at minimum" text/number/date/boolean/single_select;
this supports the broader set of common PostgreSQL scalar types a
Postgres-literate implementation team would expect a "choose the column
type" picker to offer (see app/schemas/master.py's MasterFieldType for the
full list and PG_TYPE_CATALOG below for how the frontend groups them).
Deliberately NOT covered: arrays, composite/enum types, geometric types,
full-text search (tsvector), money, xml, network types beyond inet, bit
strings — master_records stores field_values as JSONB rather than real
per-column Postgres types, so these are validated as metadata-described
JSON shapes, not enforced by an actual typed column; the exotic types
above would need dedicated input UI beyond a type-name picker to be
useful, not just a validation rule.

"reference" fields (a field that links to another Master, e.g. State.country
-> Country) need an actual database lookup to confirm the linked record(s)
exist, which is why this whole module is async and DB-aware rather than the
pure-function form it started as.
"""

import ipaddress
import uuid as uuid_module
from datetime import date, datetime, time
from typing import Any

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.form_validation import GEOLOCATION_ERROR, parse_geolocation
from app.crud.user_picker import list_picker_users
from app.models.master import MasterRecord
from app.models.upload import UploadedFile
from app.models.workflow import FormSubmission

_INTEGER_RANGES = {
    "smallint": (-32768, 32767),
    "integer": (-2147483648, 2147483647),
    "bigint": (-9223372036854775808, 9223372036854775807),
}


def _check_type(field: dict, value: Any) -> str | None:
    field_type = field["type"]

    if field_type in ("text", "varchar", "char"):
        if not isinstance(value, str):
            return "must be text"
        max_length = field.get("max_length")
        if max_length and len(value) > max_length:
            return f"must be at most {max_length} characters"

    elif field_type in _INTEGER_RANGES:
        if isinstance(value, bool) or not isinstance(value, int):
            return "must be a whole number"
        lo, hi = _INTEGER_RANGES[field_type]
        if not (lo <= value <= hi):
            return f"must be between {lo} and {hi}"

    elif field_type in ("number", "numeric", "real", "double_precision"):
        if isinstance(value, bool) or not isinstance(value, (int, float)):
            return "must be a number"

    elif field_type == "boolean":
        if not isinstance(value, bool):
            return "must be true or false"

    elif field_type == "date":
        if not isinstance(value, (date, datetime)):
            if not isinstance(value, str):
                return "must be an ISO date (YYYY-MM-DD)"
            try:
                date.fromisoformat(value[:10])
            except ValueError:
                return "must be an ISO date (YYYY-MM-DD)"

    elif field_type == "time":
        if not isinstance(value, time):
            if not isinstance(value, str):
                return "must be an ISO time (HH:MM[:SS])"
            try:
                time.fromisoformat(value)
            except ValueError:
                return "must be an ISO time (HH:MM[:SS])"

    elif field_type in ("timestamp", "timestamptz"):
        if not isinstance(value, datetime):
            if not isinstance(value, str):
                return "must be an ISO timestamp"
            try:
                datetime.fromisoformat(value.replace("Z", "+00:00"))
            except ValueError:
                return "must be an ISO timestamp"

    elif field_type == "interval":
        if not isinstance(value, str) or not value.strip():
            return "must be an interval string (e.g. '3 days', 'P1DT4H')"

    elif field_type == "uuid":
        if not isinstance(value, str):
            return "must be a UUID"
        try:
            uuid_module.UUID(value)
        except ValueError:
            return "must be a valid UUID"

    elif field_type == "inet":
        if not isinstance(value, str):
            return "must be an IP address or CIDR"
        try:
            ipaddress.ip_network(value, strict=False)
        except ValueError:
            return "must be a valid IP address or CIDR (e.g. 10.0.0.1 or 10.0.0.0/24)"

    elif field_type == "geolocation":
        if parse_geolocation(value) is None:
            return GEOLOCATION_ERROR

    elif field_type in ("json", "jsonb"):
        pass  # already parsed JSON by the time it's here — any shape is valid

    elif field_type in ("single_select", "reference", "user"):
        pass  # membership/link checked separately — see validate_field_values

    return None


async def _check_reference(
    db: AsyncSession, tenant_id: uuid_module.UUID, field: dict, value: Any
) -> list[str]:
    multi = bool(field.get("multi"))
    raw_ids = value if multi else [value]
    if multi and not isinstance(value, list):
        return ["must be a list of linked record ids"]

    parsed_ids: list[uuid_module.UUID] = []
    for raw_id in raw_ids:
        try:
            parsed_ids.append(uuid_module.UUID(str(raw_id)))
        except (ValueError, AttributeError):
            return [f"'{raw_id}' is not a valid linked record id"]

    if not parsed_ids:
        return []

    if field.get("reference_form_id"):
        reference_form_id = uuid_module.UUID(str(field["reference_form_id"]))
        form_stmt = select(FormSubmission.id).where(
            FormSubmission.tenant_id == tenant_id,
            FormSubmission.form_id == reference_form_id,
            FormSubmission.is_deleted.is_(False),
            FormSubmission.id.in_(parsed_ids),
        )
        found = {row[0] for row in (await db.execute(form_stmt)).all()}
        missing = [str(i) for i in parsed_ids if i not in found]
        if missing:
            return [f"references submission(s) not found in the linked form: {missing}"]
        return []

    reference_master_id = uuid_module.UUID(str(field["reference_master_id"]))
    stmt = select(MasterRecord.id).where(
        MasterRecord.tenant_id == tenant_id,
        MasterRecord.master_id == reference_master_id,
        MasterRecord.is_deleted.is_(False),
        MasterRecord.id.in_(parsed_ids),
    )
    found = {row[0] for row in (await db.execute(stmt)).all()}
    missing = [str(i) for i in parsed_ids if i not in found]
    if missing:
        return [f"references record(s) not found in the linked datasource: {missing}"]
    return []


async def _check_user(db: AsyncSession, tenant_id: uuid_module.UUID, field: dict, value: Any) -> str | None:
    try:
        user_id = uuid_module.UUID(str(value))
    except (ValueError, AttributeError):
        return "must be a valid user id"
    role_id = uuid_module.UUID(str(field["user_role_id"])) if field.get("user_role_id") else None
    pool = await list_picker_users(db, tenant_id, field["user_type"], role_id=role_id)
    if not any(u.id == user_id for u in pool):
        return "references a user not found in this field's allowed pool"
    return None


async def _check_file(db: AsyncSession, tenant_id: uuid_module.UUID, value: Any) -> str | None:
    try:
        file_id = uuid_module.UUID(str(value))
    except (ValueError, AttributeError):
        return "must be a valid uploaded file id"
    stmt = select(UploadedFile.id).where(UploadedFile.tenant_id == tenant_id, UploadedFile.id == file_id)
    found = (await db.execute(stmt)).scalar_one_or_none()
    if found is None:
        return "references a file that was not found (upload it via POST /uploads first)"
    return None


async def validate_field_values(
    db: AsyncSession, tenant_id: uuid_module.UUID, field_schema: list[dict], field_values: dict[str, Any]
) -> list[str]:
    errors: list[str] = []
    known_names = {f["name"] for f in field_schema}

    for field in field_schema:
        name = field["name"]
        value = field_values.get(name)

        if field["type"] == "formula":
            # Server-computed after this validation pass (see
            # app/crud/formula.py) — a client-supplied value here would let
            # a caller override the computed result, so reject it outright
            # rather than validate one.
            if name in field_values:
                errors.append(f"'{name}' is computed and cannot be set directly")
            continue
        if field["type"] == "autonumber":
            # Given out on create and kept after (app/crud/field_constraints.py)
            # — whatever a caller sends is replaced, not checked.
            continue

        if field.get("required") and (value is None or value == "" or value == []):
            custom = (field.get("required_message") or "").strip()
            errors.append(custom.replace("{label}", field.get("alias") or name) if custom else f"'{name}' is required")
            continue
        if value is None:
            continue

        if field["type"] == "reference":
            errors.extend(f"'{name}' {e}" for e in await _check_reference(db, tenant_id, field, value))
            continue

        if field["type"] == "file":
            file_error = await _check_file(db, tenant_id, value)
            if file_error:
                errors.append(f"'{name}' {file_error}")
            continue

        if field["type"] == "user":
            user_error = await _check_user(db, tenant_id, field, value)
            if user_error:
                errors.append(f"'{name}' {user_error}")
            continue

        type_error = _check_type(field, value)
        if type_error:
            errors.append(f"'{name}' {type_error}")
        elif field["type"] == "single_select":
            options = field.get("options") or []
            if options and value not in options:
                errors.append(f"'{name}' must be one of {options}")

    errors.extend(_rule_errors(field_schema, field_values, errors))

    unknown = set(field_values) - known_names
    if unknown:
        errors.append(f"unknown field(s): {sorted(unknown)}")

    return errors


def _rule_errors(field_schema: list[dict], field_values: dict[str, Any], already: list[str]) -> list[str]:
    """Each field's validation rules (Datasources › field › Validation) —
    the same rules and messages a Form field's use (app/core/form_validation.py).
    A field already reported above (wrong type, not an option, …) isn't
    checked again."""
    from app.core.form_validation import validate_field_values as check_rules

    ruled = [
        {"name": f["name"], "label": f.get("alias") or f["name"], "type": f.get("type"), "validation_rules": f.get("validation_rules") or []}
        for f in field_schema
        if f.get("validation_rules") and f.get("type") != "formula"
    ]
    if not ruled:
        return []
    reported = {e.split("'")[1] for e in already if e.startswith("'") and e.count("'") >= 2}
    return [f"{next(f['label'] for f in ruled if f['name'] == name)}: {message}" for name, message in check_rules(ruled, field_values).items() if name not in reported]
