"""Versioned restriction/safety knowledge validation and private seeding."""

from __future__ import annotations

import json
import re
import unicodedata
from datetime import date
from pathlib import Path

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

from checkin_cli.nutrition_onboarding_contract import canonical_digest
from checkin_cli.nutrition_onboarding_fs import (
    atomic_write_private_json,
    validate_profile_path,
)


class KBModel(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)


class KBSource(KBModel):
    source_id: str = Field(min_length=1, max_length=80)
    publisher: str = Field(min_length=1, max_length=200)
    title: str = Field(min_length=1, max_length=300)
    url: str = Field(min_length=1, max_length=500)
    retrieved_on: date
    scope: str = Field(min_length=1, max_length=500)


class KBRule(KBModel):
    rule_id: str = Field(min_length=1, max_length=100)
    category: str = Field(min_length=1, max_length=80)
    terms: tuple[str, ...] = Field(min_length=1, max_length=100)
    action: str = Field(min_length=1, max_length=80)
    source_ids: tuple[str, ...] = Field(min_length=1, max_length=20)


class RestrictionKnowledgeBase(KBModel):
    schema_version: str = Field(pattern=r"^nutrition_restriction_kb_v1$")
    effective_date: date
    expires_on: date
    approved: bool
    sources: tuple[KBSource, ...] = Field(min_length=1)
    rules: tuple[KBRule, ...] = Field(min_length=1)
    owner_digest: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
    approved_at: date | None = None
    digest: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")

    @model_validator(mode="after")
    def validate_dates(self) -> "RestrictionKnowledgeBase":
        lifetime = (self.expires_on - self.effective_date).days
        if lifetime < 0 or lifetime > 365:
            raise ValueError("restriction KB expiry cannot exceed one year")
        return self


class RestrictionReconciliation(KBModel):
    resolved: tuple[str, ...]
    unresolved: tuple[str, ...]
    actions: tuple[str, ...]
    requires_human_review: bool


class SeedResult(KBModel):
    runtime_path: Path
    digest: str
    committed: bool


_HUMAN_ONLY = {"medication", "condition", "pregnancy", "eating_disorder_risk"}


def load_restriction_kb_template() -> dict[str, object]:
    path = Path(__file__).with_name("policies") / "nutrition-restriction-kb-template-v1.json"
    value = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(value, dict):
        raise ValueError("restriction KB template must be an object")
    return value


def validate_restriction_kb_template(
    document: dict[str, object],
    *,
    as_of: date,
) -> RestrictionKnowledgeBase:
    try:
        kb = RestrictionKnowledgeBase.model_validate(document)
    except ValidationError as exc:
        raise ValueError(str(exc)) from exc
    if kb.approved or kb.owner_digest is not None or kb.digest is not None:
        raise ValueError("source template must be unapproved")
    if not kb.effective_date <= as_of <= kb.expires_on:
        raise ValueError("restriction KB template is not currently effective")
    source_ids = {source.source_id for source in kb.sources}
    if len(source_ids) != len(kb.sources):
        raise ValueError("restriction KB source IDs must be unique")
    rule_ids = {rule.rule_id for rule in kb.rules}
    if len(rule_ids) != len(kb.rules):
        raise ValueError("restriction KB rule IDs must be unique")
    for rule in kb.rules:
        if not set(rule.source_ids) <= source_ids:
            raise ValueError("restriction KB rule cites an unknown source")
        if rule.category in _HUMAN_ONLY and rule.action != "require_human_review":
            raise ValueError("medical and safety rules may only require human review")
    return kb


def reconcile_restriction_terms(
    kb: RestrictionKnowledgeBase,
    values: dict[str, list[str]],
) -> RestrictionReconciliation:
    category_map = {
        "allergies": "allergen",
        "intolerances": "intolerance",
        "religious_ethical_exclusions": "religious_ethical",
        "conditions": "condition",
        "medications": "medication",
    }
    resolved: list[str] = []
    unresolved: list[str] = []
    actions: list[str] = []
    for input_category, terms in values.items():
        category = category_map.get(input_category, input_category)
        rules = [rule for rule in kb.rules if rule.category == category]
        for raw in terms:
            term = _normalize(raw)
            matched = next(
                (
                    rule
                    for rule in rules
                    if "*" in rule.terms
                    or term.casefold() in {item.casefold() for item in rule.terms}
                ),
                None,
            )
            if matched is None:
                unresolved.append(term)
            else:
                resolved.append(term)
                actions.append(matched.action)
    return RestrictionReconciliation(
        resolved=tuple(resolved),
        unresolved=tuple(unresolved),
        actions=tuple(actions),
        requires_human_review=bool(unresolved)
        or "require_human_review" in actions,
    )


def seed_restriction_kb(
    *,
    profile_root: Path,
    source: Path,
    owner_digest: str,
    commit: bool,
    as_of: date,
) -> SeedResult:
    if not re.fullmatch(r"[0-9a-f]{64}", owner_digest):
        raise ValueError("owner_digest must be a lowercase SHA-256 digest")
    value = json.loads(Path(source).read_text(encoding="utf-8"))
    if not isinstance(value, dict):
        raise ValueError("restriction KB source must be an object")
    kb = validate_restriction_kb_template(value, as_of=as_of)
    payload = _readiness_runtime_payload(kb, owner_digest=owner_digest, as_of=as_of)
    payload["digest"] = canonical_digest(payload)
    runtime = (
        Path(profile_root)
        / "data"
        / "global"
        / "nutrition-safety"
        / "restriction-kb-v1.json"
    )
    if commit:
        validate_profile_path(runtime, Path(profile_root))
        runtime.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
        runtime.parent.chmod(0o700)
        atomic_write_private_json(runtime, payload)
    return SeedResult(
        runtime_path=runtime,
        digest=str(payload["digest"]),
        committed=commit,
    )


def _normalize(value: str) -> str:
    return re.sub(r"\s+", " ", unicodedata.normalize("NFC", value)).strip()


def _readiness_runtime_payload(
    kb: RestrictionKnowledgeBase,
    *,
    owner_digest: str,
    as_of: date,
) -> dict[str, object]:
    sources = [source.model_dump(mode="json") for source in kb.sources]

    def rules(category: str, *, action: str | None = None) -> list[dict[str, object]]:
        selected = [rule for rule in kb.rules if rule.category == category]
        return [
            {
                "rule_id": rule.rule_id,
                "action": action or rule.action,
                "severity": "high",
                "applicability": "all",
                "source_ids": list(rule.source_ids),
            }
            for rule in selected
        ]

    fallback_source = kb.sources[0].source_id
    return {
        "schema_version": "1.0",
        "knowledge_version": kb.effective_date.isoformat(),
        "effective_at_kst": f"{kb.effective_date.isoformat()}T00:00:00+09:00",
        "reviewed_at_kst": f"{as_of.isoformat()}T00:00:00+09:00",
        "reviewed_by": owner_digest,
        "approved": True,
        "sources": sources,
        "allergens": rules("allergen", action="exclude"),
        "intolerances": rules("intolerance", action="require_human_review"),
        "religious_ethical_exclusions": [
            {
                "rule_id": "explicit-religious-ethical-exclusion-v1",
                "action": "exclude",
                "severity": "high",
                "applicability": "customer_declared_only",
                "source_ids": [fallback_source],
            }
        ],
        "medication_condition_rules": (
            rules("medication") + rules("condition")
        ),
        "hard_contraindications": (
            rules("pregnancy") + rules("eating_disorder_risk")
        ),
        "cross_contact_rules": rules("allergen", action="exclude"),
        "substitution_rules": [
            {
                "rule_id": "reviewed-substitution-required-v1",
                "action": "inform",
                "severity": "low",
                "applicability": "after_exclusion_review",
                "source_ids": [fallback_source],
            }
        ],
        "source_template": kb.model_dump(mode="json"),
        "owner_digest": owner_digest,
        "approved_at": as_of.isoformat(),
    }
