"""Strict declarations in p2hs/email-templates.json; invalid input blocks deployment.

Cloud (apps/pc-gateway/src/lib/email-template-catalog-meta.ts) owns runtime catalog
metadata and default copy. This module is the validation mirror: known keys,
optional flags, protected placeholders, and the closed variable vocabulary.
HTML escaping and missing-variable render failure live only in Cloud render.
"""
from __future__ import annotations

import json
import os
import re
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, model_validator

MANIFEST_RELPATH = "p2hs/email-templates.json"
_META_PATH = os.path.join(os.path.dirname(__file__), "email_template_catalog.meta.json")
_PLACEHOLDER = re.compile(r"\{\{\s*([a-z_]+)\s*\}\}", re.I)

with open(_META_PATH, encoding="utf-8") as _meta_source:
    EMAIL_TEMPLATE_CATALOG: dict[str, dict[str, Any]] = json.load(_meta_source)

OVERLAY_SYNC_CONTRACT = "replace_all"
# Missing p2hs/email-templates.json → no push (leave Cloud state).
# File present with templates: [] → push empty set (all keys use platform default).
# File present with keys → Cloud D1 becomes exactly those overrides.


def declared_variable_names(catalog_entry: dict[str, Any]) -> set[str]:
    names = set(catalog_entry.get("required_variables") or [])
    names.update((catalog_entry.get("optional_variables") or {}).keys())
    return names


def unknown_placeholders(*fields: str | None, allowed: set[str]) -> list[str]:
    found: set[str] = set()
    for field in fields:
        if not field:
            continue
        for match in _PLACEHOLDER.finditer(field):
            name = match.group(1).lower()
            if name not in allowed:
                found.add(name)
    return sorted(found)


class EmailTemplateDeclaration(BaseModel):
    model_config = ConfigDict(extra="forbid", strict=True)
    template_key: str
    subject: str = Field(min_length=1)
    text_body: str = Field(min_length=1)
    html_body: str | None = None
    enabled: bool = True

    @model_validator(mode="after")
    def validate_known_and_protected(self):
        catalog_entry = EMAIL_TEMPLATE_CATALOG.get(self.template_key)
        if catalog_entry is None:
            raise ValueError(
                f"Unknown template_key {self.template_key!r}. "
                f"Known: {sorted(EMAIL_TEMPLATE_CATALOG)}"
            )
        missing = [
            token for token in catalog_entry["protected_placeholders"]
            if token not in self.text_body or (self.html_body is not None and token not in self.html_body)
        ]
        if missing:
            raise ValueError(
                f"{self.template_key}: removing {missing} breaks the feature this email is part "
                "of (the link/code it delivers). Restyle around it, don't delete it."
            )
        unknown = unknown_placeholders(
            self.subject,
            self.text_body,
            self.html_body,
            allowed=declared_variable_names(catalog_entry),
        )
        if unknown:
            raise ValueError(
                f"{self.template_key}: unknown template variable(s) {unknown}. "
                f"Allowed: {sorted(declared_variable_names(catalog_entry))}"
            )
        return self


class EmailTemplatesManifest(BaseModel):
    """Declarative desired-state overlay. Non-empty or empty templates list is
    replace-all against Cloud. Omitted keys revert to platform default."""

    model_config = ConfigDict(extra="ignore", strict=True)
    version: int = 1
    templates: list[EmailTemplateDeclaration] = Field(default_factory=list)

    @model_validator(mode="after")
    def unique_keys(self):
        keys = [t.template_key for t in self.templates]
        if len(set(keys)) != len(keys):
            raise ValueError("Duplicate template_key entries")
        return self


def validate_manifest(data: object) -> dict:
    return EmailTemplatesManifest.model_validate(data).model_dump(exclude_none=True)


def manifest_path(repo_path: str) -> str:
    return os.path.join(repo_path, *MANIFEST_RELPATH.split("/"))


def load_manifest_from_repo_path(repo_path: str) -> dict | None:
    path = manifest_path(repo_path)
    if not os.path.isfile(path):
        return None
    with open(path, encoding="utf-8") as source:
        return validate_manifest(json.load(source))


async def apply_email_templates(client, project_id: str, declaration: dict) -> list[dict]:
    """Push validated desired-state overrides. Callers must not invoke this when the
    overlay file is absent. An empty templates list clears Cloud overrides."""
    templates = declaration.get("templates") or []
    await client.push_email_templates(project_id, templates)
    return [{"template_key": t["template_key"], "ok": True} for t in templates]
