"""Strict additive declarations in p2hs/provision.json; invalid input blocks deployment."""
from __future__ import annotations

import json
import os
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator

from src.domain.codegen.entity_catalog import (
    catalog_keys,
    colliding_existing_key,
    compact_entities,
)

MANIFEST_RELPATH = "p2hs/provision.json"

class ProvisionField(BaseModel):
    model_config = ConfigDict(extra="forbid", strict=True)
    key: str = Field(pattern=r"^[a-z][a-z0-9_]{0,62}$")
    label: str = Field(min_length=1)
    type: Literal["text", "number", "boolean", "date", "select", "multi_select", "relation", "json"]
    required: bool = False
    unique: bool = False
    options: list[str] | None = None
    relation_entity_key: str | None = None

    @model_validator(mode="after")
    def validate_type_details(self):
        if self.type in {"select", "multi_select"} and not self.options:
            raise ValueError("Select fields require options")
        if self.type == "relation" and not self.relation_entity_key:
            raise ValueError("Relation fields require relation_entity_key")
        return self

class ProvisionEntity(BaseModel):
    model_config = ConfigDict(extra="forbid", strict=True)
    key: str = Field(pattern=r"^[a-z][a-z0-9_]{0,62}$")
    label: str = Field(min_length=1)
    fields: list[ProvisionField]
    rls_enabled: bool = False

    @model_validator(mode="after")
    def unique_fields(self):
        if len({f.key for f in self.fields}) != len(self.fields):
            raise ValueError("Duplicate field keys")
        return self

class ProvisionBucket(BaseModel):
    model_config = ConfigDict(extra="forbid", strict=True)
    name: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,62}$")
    access_policy: Literal["public", "private"] = "private"
    rls_enabled: bool = False

class ProvisionManifest(BaseModel):
    # Apps often keep leftover `auth` / seed-user keys in the same file.
    # Core only provisions entities and storage buckets.
    model_config = ConfigDict(extra="ignore", strict=True)
    version: Literal[1] = 1
    entities: list[ProvisionEntity] = Field(default_factory=list)
    storage_buckets: list[ProvisionBucket] = Field(default_factory=list)

    @model_validator(mode="after")
    def unique_resources(self):
        if len({e.key for e in self.entities}) != len(self.entities):
            raise ValueError("Duplicate entity keys")
        if len({b.name for b in self.storage_buckets}) != len(self.storage_buckets):
            raise ValueError("Duplicate bucket names")
        return self

def validate_manifest(data: object) -> dict:
    return ProvisionManifest.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))

def assert_compatible_entity(declared: dict, existing: dict) -> list[dict]:
    """Raise on incompatible live fields. Return declared fields that are additive."""
    if existing.get("status", "active") != "active":
        raise ValueError(f"Entity {declared['key']} is not active")
    if declared["rls_enabled"] != existing.get("rls_enabled", False):
        raise ValueError(f"Entity {declared['key']} has incompatible access policy")
    fields = {f["key"]: f for f in existing["fields"]}
    additive: list[dict] = []
    for field in declared["fields"]:
        live = fields.get(field["key"])
        if live is None:
            if field.get("required") or field.get("unique"):
                raise ValueError("Adding required or unique fields needs an explicit migration")
            additive.append(field)
            continue
        for key in ("type", "required", "unique", "options", "relation_entity_key"):
            default = False if key in {"required", "unique"} else None
            if field.get(key, default) != live.get(key, default):
                raise ValueError(f"Incompatible field {declared['key']}.{field['key']}: {key}")
    return additive


def merged_entity_schema(existing: dict, additive: list[dict]) -> dict:
    live_fields = list(existing.get("fields") or [])
    known = {f["key"] for f in live_fields}
    fields = live_fields + [f for f in additive if f["key"] not in known]
    schema = existing.get("schema") if isinstance(existing.get("schema"), dict) else {}
    return {**schema, "fields": fields}

async def provision_entities(
    client,
    project_id: str,
    declarations: list[dict],
    *,
    known_entities: list[dict] | None = None,
    protected_entity_keys: set[str] | None = None,
) -> list[dict]:
    if not declarations:
        return []
    protected = set(protected_entity_keys or ())
    known = compact_entities(known_entities or [])
    try:
        # Preserve the full live schema and field metadata when adding fields.
        # The compact prompt catalog is not a safe input to a schema PATCH.
        live = (await client.get_project_schema(project_id))["entities"]
    except Exception as exc:
        raise ValueError(
            "Cloud schema is unavailable; refusing to create or replace entities"
        ) from exc
    existing = {e["key"]: e for e in live if e.get("status", "active") == "active"}
    inactive_live_keys = {e["key"] for e in live if e.get("status", "active") != "active"}
    known_keys = catalog_keys(live, known)
    pending_updates: dict[str, list[dict]] = {}
    for declaration in declarations:
        key = declaration["key"]
        if key in protected:
            raise ValueError(
                f"Entity '{key}' is owned by an installed Feature Pack. "
                "Pack install is the writer for that schema."
            )
        collision = colliding_existing_key(key, known_keys)
        if collision:
            raise ValueError(
                f"Entity '{key}' collides with existing '{collision}'. "
                "Reuse the existing entity_key; do not create a second table."
            )
        if key in existing:
            additive = assert_compatible_entity(declaration, existing[key])
            if additive:
                pending_updates[key] = additive
        elif key in inactive_live_keys:
            raise ValueError(
                f"Entity '{key}' already exists in the project catalog and is not active. "
                "Reuse that key; do not provision a replacement table."
            )
        elif key in catalog_keys(known):
            raise ValueError(
                f"Previously known entity '{key}' is missing from Cloud; restore or investigate it, "
                "do not silently recreate an empty replacement."
            )
    outcomes = []
    for declaration in declarations:
        key = declaration["key"]
        if key not in existing:
            await client.create_project_entity(
                project_id,
                {
                    "entity_key": key,
                    "name": declaration["label"],
                    "plural_name": declaration["label"],
                    "schema": {"fields": declaration["fields"]},
                    "settings": {"rls_enabled": declaration["rls_enabled"]},
                },
            )
            outcomes.append({"entity": key, "ok": True, "action": "created"})
            continue
        if key in pending_updates:
            await client.patch_project_entity(project_id, key, {
                "schema": merged_entity_schema(existing[key], pending_updates[key]),
            })
            outcomes.append({"entity": key, "ok": True, "action": "updated"})
            continue
        outcomes.append({"entity": key, "ok": True, "action": "existing"})
    # 409 / PATCH is not proof of compatibility: verify schema accepted by Cloud.
    accepted = {e["key"]: e for e in (await client.get_project_schema(project_id))["entities"]}
    for declaration in declarations:
        if declaration["key"] not in accepted:
            raise ValueError(f"Cloud did not provision entity {declaration['key']}")
        leftover = assert_compatible_entity(declaration, accepted[declaration["key"]])
        if leftover:
            names = ", ".join(f["key"] for f in leftover)
            raise ValueError(f"Cloud did not provision fields on {declaration['key']}: {names}")
    return outcomes


async def installed_pack_entity_keys(db, project_id) -> set[str]:
    """Entity keys an installed Feature Pack already owns for this project."""
    from sqlalchemy import select

    from src.domain.feature_packs.models import FeaturePack, ProjectFeaturePack
    from src.domain.feature_packs.normalize import load_pack_contract

    rows = (
        await db.execute(
            select(FeaturePack.slug, FeaturePack.composition)
            .join(ProjectFeaturePack, ProjectFeaturePack.pack_slug == FeaturePack.slug)
            .where(ProjectFeaturePack.project_id == project_id)
        )
    ).all()
    keys: set[str] = set()
    for slug, composition in rows:
        contract = load_pack_contract(
            slug, composition if isinstance(composition, dict) else None
        )
        for entity in contract.entities:
            if entity.entity_key:
                keys.add(entity.entity_key)
    return keys
