"""Level 1 capability composition: turn a CompositionRequirement into an installable
FeaturePack row, or an honest PrimitiveGap -- never a fake success.

Two functions, deliberately separated:
- `derive_pack_composition` is pure (no DB) and does every check that's derivable from
  the requirement alone -- the model never picks the enforcement mechanism, this
  function does, mechanically, from three confirmed access patterns (see the module
  docstring in requirements_spec.py):
    1. rls_enabled           -- a "member" actor reads/updates its own rows
    2. plain entity + the    -- an "admin"/"super_admin" actor reads/updates via the
       existing staff-only      existing staff-only generic /api/records/:entity port
       records port             (no owner filtering -- "all" scope is inherent to it)
    3. plain entity + the    -- an "anonymous" actor creates via the Step 0 generic
       forms primitive          public-create primitive (p2hs-forms.mjs)
  `anonymous` may only ever appear paired with `action="create"` -- anonymous read/
  update/delete of an arbitrary composed entity has no supported primitive and is a
  hard validation failure, not a primitive_gap (the requirement itself is invalid,
  not merely unsupported).
- `compose_and_register_pack` does the DB-dependent checks (function_slugs/
  connector_actions must already exist) and, on success, delegates to the existing,
  unmodified `PackInstallService.install_pack` -- this module never provisions a
  Cloud entity or installs anything itself.
"""
from __future__ import annotations

import hashlib
import json
from typing import Any, Literal, Optional
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field

from src.domain.codegen.provision_manifest import ProvisionField
from src.domain.feature_packs.contract import validate_workflow_transitions
from src.domain.feature_packs.ownership import validate_composition_ownership_contracts
from src.domain.feature_packs.requirements_spec import (
    SUPPORTED_PRIMITIVES,
    CompositionRequirement,
    WorkflowRequirement,
)

RoleName = Literal["anonymous", "member", "admin", "super_admin"]


class RelationshipScope(BaseModel):
    """Declarative relationship predicate compiled for Cloud enforcement."""

    model_config = ConfigDict(extra="forbid", strict=True)

    type: Literal["relationship"] = "relationship"
    relation_entity_key: str = Field(pattern=r"^[a-z][a-z0-9_]{0,62}$")
    subject_entity_field: str = Field(pattern=r"^[a-z][a-z0-9_]{0,62}$")
    subject_record_field: str = Field(pattern=r"^[a-z][a-z0-9_]{0,62}$")
    principal_field: str = Field(pattern=r"^[a-z][a-z0-9_]{0,62}$")
    active_field: str | None = Field(default=None, pattern=r"^[a-z][a-z0-9_]{0,62}$")
    active_values: list[str] = Field(default_factory=list)


class AccessPolicyEntry(BaseModel):
    """One (role, action) grant. `effect` is always "allow" -- there is no explicit
    "deny" entry; anything not present is implicitly denied (see checkAccessPolicy on
    the Cloud side). `scope` is None for `create` (nothing to scope against yet)."""
    effect: Literal["allow"] = "allow"
    scope: Optional[Literal["owner", "all"] | RelationshipScope] = None


class CompiledAccessPolicy(BaseModel):
    """Versioned, compiled authorization policy -- see requirements_spec.py's
    module docstring and composer.py's derive_pack_composition for how this is built.
    Cloud (checkAccessPolicy in records.ts) does an exact lookup against exactly this
    shape, no role-hierarchy reasoning of its own: `super_admin`'s action set already
    includes everything `admin` has by the time this is constructed here, not because
    Cloud special-cases the name. `anonymous` never appears as a role key -- it's
    routed entirely through the form.public_create primitive instead."""
    version: Literal[1] = 1
    roles: dict[str, dict[str, AccessPolicyEntry]]


class ComposedEntity(BaseModel):
    key: str
    label: str
    fields: list[ProvisionField]
    rls_enabled: bool = False
    workflow: Optional[WorkflowRequirement] = None
    access_policy: Optional[CompiledAccessPolicy] = None


class PackCompositionSpec(BaseModel):
    slug: str
    name: str
    entities: list[ComposedEntity]
    access_policy: CompiledAccessPolicy
    function_slugs: list[str] = Field(default_factory=list)
    requires: list[str] = Field(default_factory=list)
    source: Literal["synthesized"] = "synthesized"
    requirement_fingerprint: str


class PrimitiveGap(BaseModel):
    """An honest "Cloud can't express this yet" result -- never a trigger to fall back
    to generated/executable code. This is deliberately a stop, not an escalation: what
    happens next (is this generic enough to become a new Level 2 primitive, per the
    checklist on SUPPORTED_PRIMITIVES; is it inherently bespoke and a future Level 3
    candidate) is a human engineering decision, made outside this module.

    Kept structured/machine-readable (not just a log line) on purpose: today the
    caller (codegen.py) only logs it, but the same shape is what a future
    gap-clustering pass ("which primitives get requested most") would consume without
    any change to this API -- that pass isn't built, this shape just doesn't block it.

    `criticality` (from the originating CompositionRequirement) is what lets
    codegen.py's run-status selection distinguish a run that must be reported as
    "blocked" (a required capability wasn't fulfilled) from one that's merely
    "completed_with_limitations" (an optional one wasn't)."""
    model_config = ConfigDict(extra="forbid")
    status: Literal["primitive_gap"] = "primitive_gap"
    missing: list[str]
    requirement_name: str
    criticality: Literal["required", "optional"] = "required"


def _fingerprint(req: CompositionRequirement) -> str:
    """Normalized hash of (requirements, field keys, workflow) -- exact-match dedup
    only for v1 (see composer.py's module docstring / the plan's explicitly-deferred
    section for why full semantic similarity isn't attempted here). Workflow is
    included so two requirements with the same actors/fields but different transition
    graphs (e.g. a stricter ticket lifecycle) never collide onto the same synthesized
    pack."""
    canonical = {
        "requirements": sorted(
            (r.action, r.resource, r.actor) for r in req.requirements
        ),
        "fields": sorted(f.key for f in req.fields),
        "workflow": (
            {"field": req.workflow.field, "transitions": req.workflow.transitions}
            if req.workflow
            else None
        ),
    }
    return hashlib.sha256(json.dumps(canonical, sort_keys=True).encode()).hexdigest()[:32]


def _cloud_entity_dict(entity: ComposedEntity) -> dict[str, Any]:
    """Translate a ComposedEntity into the shape Cloud's entity-create API actually
    expects -- entity_key/name/plural_name/schema.fields[], top-level rls_enabled --
    mirroring the proven translation in provision_manifest.py::provision_entities.
    ComposedEntity.model_dump() alone does NOT match Cloud's API (key/label vs
    entity_key/name, no plural_name, fields not nested under schema) and silently
    produced entities install_pack could never provision -- see the Level 2 plan's
    Step 0 for how this was found."""
    label = entity.label
    schema: dict[str, Any] = {
        "fields": [
            {
                "key": f.key,
                "name": f.label,
                "type": f.type,
                "required": f.required,
                **({"options": f.options} if f.options else {}),
                **({"relation_entity_key": f.relation_entity_key} if f.relation_entity_key else {}),
            }
            for f in entity.fields
        ],
    }
    if entity.workflow:
        schema["workflow"] = entity.workflow.model_dump()
    if entity.access_policy:
        schema["access_policy"] = entity.access_policy.model_dump()
    return {
        "entity_key": entity.key,
        "name": label,
        "plural_name": label,
        "schema": schema,
        "rls_enabled": entity.rls_enabled,
    }


def derive_pack_composition(
    req: CompositionRequirement, project_ref: str
) -> PackCompositionSpec | PrimitiveGap:
    actor_actions: dict[RoleName, set[str]] = {}
    for r in req.requirements:
        actor_actions.setdefault(r.actor, set()).add(r.action)

    if "anonymous" in actor_actions and actor_actions["anonymous"] - {"create"}:
        raise ValueError(
            "anonymous actor may only be paired with action=create -- "
            "no supported primitive for public read/update/delete"
        )

    inferred_requires = set(req.requires)
    inferred_requires.add("data.entity")
    if {"member", "admin", "super_admin"} & actor_actions.keys():
        inferred_requires.add("auth.roles")
    if "anonymous" in actor_actions:
        inferred_requires.add("form.public_create")
    if req.workflow:
        inferred_requires.add("workflow.transition")

    unsupported = inferred_requires - SUPPORTED_PRIMITIVES
    if unsupported:
        return PrimitiveGap(missing=sorted(unsupported), requirement_name=req.name, criticality=req.criticality)

    workflow: Optional[WorkflowRequirement] = None
    if req.workflow:
        field_by_key = {f.key: f for f in req.fields}
        target = field_by_key.get(req.workflow.field)
        if target is None:
            raise ValueError(
                f"workflow.field '{req.workflow.field}' is not declared in fields"
            )
        normalized_transitions = validate_workflow_transitions(
            target.type, target.options, req.workflow.transitions
        )
        workflow = WorkflowRequirement(field=req.workflow.field, transitions=normalized_transitions)

    rls_enabled = "member" in actor_actions

    # Compile the granular role x action policy Cloud actually enforces
    # (checkAccessPolicy in records.ts) -- effect + scope, not just a bare action
    # list. `member` always means "their own row" (scope=owner); staff access is
    # always unscoped (scope=all) -- matches the three confirmed access patterns.
    # `anonymous` never gets an entry here: it's routed entirely through
    # form.public_create, which doesn't consult access_policy at all.
    roles: dict[str, dict[str, AccessPolicyEntry]] = {}
    for actor, actions in actor_actions.items():
        if actor == "anonymous":
            continue
        scope = "owner" if actor == "member" else "all"
        roles[actor] = {action: AccessPolicyEntry(scope=scope) for action in sorted(actions)}

    # super_admin must never end up MORE restricted than admin by simple omission
    # from a requirement that only named "admin" -- union admin's actions in,
    # keeping any actions the requirement explicitly gave super_admin on top.
    if "admin" in roles:
        merged = dict(roles["admin"])
        merged.update(roles.get("super_admin", {}))
        roles["super_admin"] = merged

    access_policy = CompiledAccessPolicy(roles=roles)

    # Coverage proof: every requested (action, resource, actor) must be traceable to
    # the derived policy -- by construction it always is (roles is built directly
    # from actor_actions, which is built directly from req.requirements), but this
    # stays as an explicit regression guard rather than an implicit assumption.
    # anonymous is exempt -- it has no access_policy entry by design (see above).
    for r in req.requirements:
        if r.actor == "anonymous":
            continue
        if r.action not in roles.get(r.actor, {}):
            raise ValueError(
                f"derived access_policy does not cover requested {r.actor}:{r.action} on {r.resource}"
            )

    entity = ComposedEntity(
        key=req.name,
        label=req.name.replace("_", " ").title(),
        fields=req.fields,
        rls_enabled=rls_enabled,
        workflow=workflow,
        access_policy=access_policy,
    )
    validate_composition_ownership_contracts({"entities": [_cloud_entity_dict(entity)]})

    return PackCompositionSpec(
        slug=f"local-{project_ref}-{req.name}",
        name=req.name,
        entities=[entity],
        access_policy=access_policy,
        function_slugs=list(req.function_slugs),
        requires=sorted(inferred_requires),
        requirement_fingerprint=_fingerprint(req),
    )


async def compose_and_register_pack(
    db,
    cloud_client,
    project_id: UUID,
    project_ref: str,
    req: CompositionRequirement,
    user_prompt: str = "",
) -> dict[str, Any]:
    """Validate DB-dependent references, register (find-or-create) the project_local
    FeaturePack row, and install via the existing, unmodified PackInstallService."""
    from src.domain.edge_functions.repository import FunctionLibraryRepository
    from src.domain.feature_packs.models import FeaturePack
    from src.domain.feature_packs.service import PackInstallService
    from sqlalchemy import select

    for slug in req.function_slugs:
        fn = await FunctionLibraryRepository(db).get_by_slug(slug)
        if not fn or fn.status != "published":
            return {
                "name": req.name,
                "ok": False,
                "errors": [f"unknown_or_unpublished_function_slug:{slug}"],
            }

    try:
        result = derive_pack_composition(req, project_ref)
    except ValueError as exc:
        return {"name": req.name, "ok": False, "errors": [f"invalid_composition:{exc}"]}

    if isinstance(result, PrimitiveGap):
        return {
            "name": req.name,
            "ok": False,
            "status": "primitive_gap",
            "missing": result.missing,
            "criticality": result.criticality,
        }

    spec = result
    existing = (
        await db.execute(
            select(FeaturePack).where(
                FeaturePack.project_id == project_id,
                FeaturePack.requirement_fingerprint == spec.requirement_fingerprint,
                FeaturePack.scope == "project_local",
            )
        )
    ).scalar_one_or_none()

    from src.domain.feature_packs.contract import (
        FEATURE_PACK_CONTRACT_VERSION,
        typed_requires_from_primitives,
    )

    composition = {
        "schema_version": FEATURE_PACK_CONTRACT_VERSION,
        "function_slugs": spec.function_slugs,
        "entities": [_cloud_entity_dict(e) for e in spec.entities],
        "connector_actions": [],
        "requires": typed_requires_from_primitives(spec.requires),
        "optional_requires": [],
        "provider_bindings": [],
        "provides": [],
    }
    access_policy = spec.access_policy.model_dump()

    if existing:
        existing.composition = composition
        existing.access_policy = access_policy
        pack_slug = existing.slug
        await db.flush()
    else:
        pack = FeaturePack(
            slug=spec.slug,
            name=spec.name.replace("_", " ").title(),
            description=f"Synthesized for project {project_ref}: {spec.name}",
            version="0.1.0-local",
            status="active",
            scope="project_local",
            project_id=project_id,
            requirement_fingerprint=spec.requirement_fingerprint,
            composition=composition,
            access_policy=access_policy,
        )
        db.add(pack)
        await db.flush()
        pack_slug = spec.slug

    from src.domain.connectors.repository import ConnectorRepository
    from src.domain.edge_functions.repository import (
        ProjectFunctionRepository,
        ProjectFunctionTokenRepository,
        ProjectFunctionVersionRepository,
        ProjectWebhookEndpointRepository,
        WorkerRuntimeVersionRepository,
    )
    from src.domain.edge_functions.service import EdgeFunctionsService
    from src.domain.projects.repository import ProjectRepository

    edge_svc = EdgeFunctionsService(
        library_repo=FunctionLibraryRepository(db),
        fn_repo=ProjectFunctionRepository(db),
        version_repo=ProjectFunctionVersionRepository(db),
        webhook_repo=ProjectWebhookEndpointRepository(db),
        runtime_repo=WorkerRuntimeVersionRepository(db),
        token_repo=ProjectFunctionTokenRepository(db),
        project_repo=ProjectRepository(db),
        connector_repo=ConnectorRepository(db),
        cloud_client=cloud_client,
    )
    install_outcome = await PackInstallService(db, edge_svc, cloud_client).install_pack(
        project_id=project_id,
        pack_slug=pack_slug,
        config={},
        user_prompt=user_prompt,
    )
    return {"name": req.name, "pack_slug": pack_slug, **install_outcome}
