"""external.entity_ingest — the Core-side implementation of the governed
composition ``verified webhook -> bounded payload mapping -> idempotent
entity upsert``.

This is NOT a second records API and NOT a capability invoke. It exists
because the one existing generic records BFF (``p2hs-records.mjs``) is
staff-session-only, so a machine-to-machine ingestion path needs its own
route — but the actual persistence still goes through the same Cloud entity
records endpoints every other write path uses.

Trust boundary: the caller is the generated app's own BFF, authenticated with
its per-project function token (same mechanism Forms submissions use). That
proves the request came through the app's own trusted dispatch handling of a
Cloud-verified webhook; it is not re-verification of the original external
sender, and this module does not attempt that. ``source`` is therefore taken
from the Cloud-set ``provider`` field of the forwarded envelope, never from
the external payload — the payload may supply its own ``external_id`` (an
opaque dedup key), but it may not choose its own source/entity/project/
authorization scope, and it may not write any field the pack's own field
policy has not explicitly marked ``public_create``.
"""
from __future__ import annotations

from typing import Any
from uuid import UUID

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

from src.domain.feature_packs.models import FeaturePack, ProjectFeaturePack
from src.domain.feature_packs.normalize import load_pack_contract, stamp_composition
from src.integrations.cloud_control_client import CloudControlClient


class EntityIngestError(ValueError):
    def __init__(self, code: str, message: str):
        super().__init__(message)
        self.code = code


_INSTALLED_STATUSES = {"installed", "configured", "deployed", "verified"}


class EntityIngestService:
    def __init__(self, db: AsyncSession, cloud_client: CloudControlClient):
        self._db = db
        self._cloud = cloud_client

    async def ingest(
        self,
        *,
        project_id: UUID,
        pack_slug: str,
        provider: str,
        envelope: dict[str, Any],
    ) -> dict[str, Any]:
        install = await self._db.execute(
            select(ProjectFeaturePack).where(
                ProjectFeaturePack.project_id == project_id,
                ProjectFeaturePack.pack_slug == pack_slug,
            )
        )
        install_row = install.scalar_one_or_none()
        if install_row is None or install_row.status not in _INSTALLED_STATUSES:
            raise EntityIngestError("pack_not_installed", f"'{pack_slug}' is not installed for this project")

        pack = await self._db.execute(select(FeaturePack).where(FeaturePack.slug == pack_slug))
        pack_row = pack.scalar_one_or_none()
        if pack_row is None:
            raise EntityIngestError("pack_not_found", f"Unknown feature pack '{pack_slug}'")

        # Overlay (catalog_v2.py) is always re-read live so a since-updated field
        # policy or binding takes effect without needing to re-seed every project's
        # stored composition row.
        contract = load_pack_contract(pack_slug, pack_row.composition)
        binding = next(
            (b for b in contract.entity_ingest_bindings if b.webhook_provider == provider),
            None,
        )
        if binding is None:
            raise EntityIngestError(
                "binding_not_found", f"'{pack_slug}' has no entity_ingest_binding for provider '{provider}'"
            )

        stamped = stamp_composition(pack_slug, pack_row.composition)
        entity = next(
            (e for e in stamped.get("entities") or [] if e.get("entity_key") == binding.entity_key),
            None,
        )
        if entity is None:
            raise EntityIngestError("entity_not_found", f"Entity '{binding.entity_key}' is not declared")
        field_flags = {
            f["key"]: f for f in (entity.get("schema") or {}).get("fields") or [] if f.get("key")
        }

        payload = envelope.get("payload")
        if not isinstance(payload, dict):
            raise EntityIngestError("invalid_payload", "payload must be an object")

        mapped = {
            key: value
            for key, value in payload.items()
            if field_flags.get(key, {}).get("public_create") is True
        }

        source = str(envelope.get("provider") or "").strip()
        if not source:
            raise EntityIngestError("missing_provenance", "forwarded envelope is missing provider")

        identity_values: dict[str, str] = {}
        for field in binding.identity_fields:
            if field == "source":
                identity_values[field] = source
                continue
            value = payload.get(field)
            if not isinstance(value, str) or not value.strip():
                raise EntityIngestError(
                    "missing_identity", f"payload is missing required identity field '{field}'"
                )
            identity_values[field] = value.strip()

        existing = await self._cloud.query_project_records(
            str(project_id), binding.entity_key, where=identity_values, limit=1
        )
        rows = existing.get("records") if isinstance(existing, dict) else None

        if rows:
            record_id = rows[0]["id"]
            await self._cloud.update_entity_record_data(
                str(project_id), binding.entity_key, record_id, mapped
            )
            return {"ok": True, "action": "updated", "record_id": record_id}

        create_data = {**mapped, **identity_values}
        if binding.default_status_field and binding.default_status_value:
            create_data.setdefault(binding.default_status_field, binding.default_status_value)
        created = await self._cloud.create_entity_records(
            str(project_id), binding.entity_key, [{"data": create_data}]
        )
        return {"ok": True, "action": "created", "record_id": created[0]["id"]}
