"""Manifest domain — Pydantic schemas."""
from __future__ import annotations

from datetime import datetime
from typing import Any, Literal, Optional
from uuid import UUID

from pydantic import BaseModel, Field, field_validator


# ── Manifest section models ──────────────────────────────────────────────────

class ManifestProjectSection(BaseModel):
    id: str
    name: str
    workspace_id: str
    plan: str
    cloud_gateway_url: str


class ManifestCredentialsSection(BaseModel):
    cloud_service_token_env: str = "CLOUD_SERVICE_TOKEN"
    project_id_env: str = "PROJECT_ID"
    cloud_gateway_env: str = "CLOUD_GATEWAY_URL"


class ManifestEntityField(BaseModel):
    key: str
    label: str
    type: str
    required: bool = False
    unique: bool = False
    options: Optional[list[str]] = None
    relation_entity_key: Optional[str] = None


class ManifestEntity(BaseModel):
    key: str
    label: str
    fields: list[ManifestEntityField] = Field(default_factory=list)
    rls_enabled: bool = False
    record_count: Optional[int] = None
    status: str = "active"


class ManifestBackendSection(BaseModel):
    """Default backend for every project: P2HS Cloud via a thin BFF.

    Manifests are assembled fresh, so existing and new projects both receive this.
    If no customer backend is provided, the generated app uses P2HS Cloud.
    Bring-your-own-backend attach is a later feature; most apps stay on Cloud.
    """

    provider: Literal["p2hs_cloud"] = "p2hs_cloud"
    generated_app: Literal["ui_and_bff"] = "ui_and_bff"
    durable_data: Literal["p2hs_cloud"] = "p2hs_cloud"
    jobs_and_queues: Literal["p2hs_cloud"] = "p2hs_cloud"
    policy_authority: Literal["p2hs_core"] = "p2hs_core"
    uses: Literal["p2hs_cloud"] = "p2hs_cloud"
    customer_backend: Optional[str] = None
    split: dict[str, Any] = Field(
        default_factory=lambda: {
            "generated_app": [
                "UI",
                "calls to installed immutable platform /api routes",
            ],
            "p2hs_cloud": [
                "record engine (create/read/update/delete/query/upsert)",
                "app-user identities",
                "jobs and queues",
                "webhooks",
                "storage artifacts",
                "connector dispatch",
                "realtime",
            ],
            "p2hs_core": [
                "authorization",
                "tenancy",
                "entitlements",
                "billing policy",
                "secret custody",
                "this manifest",
            ],
        }
    )
    customer_backend_attach: dict[str, Any] = Field(
        default_factory=lambda: {
            "status": "not_attached",
            "uses": "p2hs_cloud",
            "when_attached": (
                "Adopt mode attaches an existing HTTP API through external_endpoints "
                "after verification. customer_backend stays null. Until that attachment "
                "is active, and whenever no adopt attachment is present, the generated "
                "app uses P2HS Cloud. Do not invent a local store or call a customer "
                "origin from the Worker."
            ),
            "codegen": (
                "If no customer backend is provided, store data in Cloud records and "
                "proxy via the BFF. Do not build SQLite/KV/JSON as a substitute."
            ),
        }
    )


class ManifestDatabaseSection(BaseModel):
    persistence: Literal["cloud_record_engine"] = "cloud_record_engine"
    schema_status: Literal["ready", "empty", "unavailable", "omitted"] = "unavailable"
    schema_source: Literal["live", "cached", "none"] = "none"
    entities: Optional[list[ManifestEntity]] = None


class ManifestAuthSection(BaseModel):
    """Role names (super_admin/admin/member) are a fixed platform convention, same
    across every generated app — see _AUTH_ROLES_PLAYBOOK in codegen/prompt_wrapper.py
    for the full three-tier contract. Paths (admin_panel_path, customer_login_path)
    are NOT fixed — they default to /admin and /login but are per-project
    configurable and sticky once a user names their own (see
    ProjectRuntimeInvariants in projects/runtime_invariants.py, and backend_guard.py's
    hardcoded-/admin-redirect rule, which exists specifically because a project's
    real admin path can differ from the default)."""
    user_directory: Literal["platform_only"] = "platform_only"
    enabled: bool
    providers: list[str] = Field(default_factory=list)
    allow_signup: bool = True
    custom_roles: list[str] = Field(default_factory=list)
    google_oauth_mode: Optional[str] = None
    super_admin_role: str = "super_admin"
    admin_role: str = "admin"
    customer_role: str = "member"
    admin_panel_path: str = "/admin"
    customer_login_path: str = "/login"
    workspace_owner_email: Optional[str] = None
    workspace_owner_name: Optional[str] = None
    staff_from_workspace: list[dict[str, Any]] = Field(default_factory=list)
    owner_provisioned_as_super_admin: bool = False
    owner_provisioned_as_admin: bool = False


class ManifestBucket(BaseModel):
    name: str
    label: str
    access_policy: str
    rls_enabled: bool = False
    cdn_url: Optional[str] = None
    max_file_size_bytes: Optional[int] = None


class ManifestStorageSection(BaseModel):
    buckets: list[ManifestBucket] = Field(default_factory=list)


class ManifestLibraryFunction(BaseModel):
    id: str
    slug: str
    name: str
    tags: list[str] = Field(default_factory=list)
    requires: list[str] = Field(default_factory=list)
    status: str
    description: Optional[str] = None
    version: Optional[str] = None
    trigger_type: Optional[str] = None
    input_schema: Optional[dict[str, Any]] = None
    output_schema: Optional[dict[str, Any]] = None
    route_examples: Optional[dict[str, Any]] = None


class ManifestFeaturePack(BaseModel):
    id: str
    slug: str
    name: str
    description: Optional[str] = None
    version: str
    tags: list[str] = Field(default_factory=list)
    composes: list[str] = Field(default_factory=list)
    requires: list[str] = Field(default_factory=list)
    provides: list[str] = Field(default_factory=list)
    events: list[str] = Field(default_factory=list)
    provider_bindings: list[dict[str, Any]] = Field(default_factory=list)
    status: str
    input_schema: Optional[dict[str, Any]] = None

    @field_validator("composes", mode="before")
    @classmethod
    def coerce_composes_to_library_slugs(cls, value: Any) -> list[str]:
        from src.domain.feature_packs.composition import library_slugs_from_entries

        if value is None:
            return []
        if isinstance(value, list) and all(isinstance(item, str) for item in value):
            return [item for item in (s.strip() for s in value) if item]
        return library_slugs_from_entries(value)

    @field_validator("requires", mode="before")
    @classmethod
    def coerce_typed_requires(cls, value: Any) -> list[str]:
        if not value:
            return []
        out: list[str] = []
        for item in value:
            if isinstance(item, str):
                token = item.strip()
                if token:
                    out.append(token)
            elif isinstance(item, dict) and item.get("id"):
                kind = str(item.get("kind") or "primitive")
                out.append(f"{kind}:{item['id']}")
        return out

    @field_validator("events", mode="before")
    @classmethod
    def coerce_event_ids(cls, value: Any) -> list[str]:
        if not value:
            return []
        out: list[str] = []
        for item in value:
            if isinstance(item, str) and item.strip():
                out.append(item.strip())
            elif isinstance(item, dict) and item.get("id"):
                out.append(str(item["id"]))
        return out


class ManifestProjectFeaturePack(BaseModel):
    slug: str
    version: str
    status: str
    config: dict[str, Any] = Field(default_factory=dict)
    blocker: Optional[dict[str, Any]] = None


class ManifestFeaturePacksSection(BaseModel):
    catalog: list[ManifestFeaturePack] = Field(default_factory=list)
    installed: list[ManifestProjectFeaturePack] = Field(default_factory=list)


class ManifestProjectFunction(BaseModel):
    slug: str
    name: str
    trigger: str
    event: Optional[str] = None
    cron: Optional[str] = None
    status: str


class ManifestFunctionsSection(BaseModel):
    system: list[ManifestLibraryFunction] = Field(default_factory=list)
    project: list[ManifestProjectFunction] = Field(default_factory=list)


class ManifestIntegrationsSection(BaseModel):
    model_config = {"extra": "allow"}


class ManifestConnectionColumn(BaseModel):
    name: str
    type: str
    primary: bool = False
    nullable: bool = True


class ManifestConnectionFK(BaseModel):
    column: str
    references: dict[str, str]


class ManifestConnectionTable(BaseModel):
    columns: list[ManifestConnectionColumn] = Field(default_factory=list)
    foreign_keys: list[ManifestConnectionFK] = Field(default_factory=list)


class ManifestConnection(BaseModel):
    type: str
    status: str
    readonly: bool = False
    schema_info: Optional[dict[str, Any]] = Field(None, alias="schema")
    model_config = {"populate_by_name": True}


class ManifestConnectionsSection(BaseModel):
    model_config = {"extra": "allow"}


class ManifestConnectorsSection(BaseModel):
    model_config = {"extra": "allow"}


class ManifestCustomChannel(BaseModel):
    name: str
    access_policy: str


class ManifestRealtimeSection(BaseModel):
    built_in_channels: list[str] = Field(
        default_factory=lambda: ["records", "jobs", "auth", "storage"]
    )
    custom_channels: list[ManifestCustomChannel] = Field(default_factory=list)


class ManifestEmbedConfig(BaseModel):
    entity: str
    field: str
    auto_embed: bool = True


class ManifestAISection(BaseModel):
    enabled: bool
    embedding_provider: Optional[str] = None
    available_models: list[str] = Field(default_factory=list)
    embed_configs: list[ManifestEmbedConfig] = Field(default_factory=list)


class ManifestSchedule(BaseModel):
    name: str
    cron: str
    fn_slug: str
    status: str


class ManifestKnowledgeEntry(BaseModel):
    title: str
    content: str


class ManifestKnowledgeBaseSection(BaseModel):
    entries: list[ManifestKnowledgeEntry] = Field(default_factory=list)


class ManifestEnvVarsSection(BaseModel):
    """Key names of project env vars configured in the platform.

    Values are never included. Split by visibility so codegen knows:
    - secrets: server-side only, access via process.env.KEY, never in client bundles.
    - public:  can appear in client-visible config (NEXT_PUBLIC_*, import.meta.env.*, etc.)
               and are also written to .env.example in the project repo.
    """
    secrets: list[str] = Field(default_factory=list)
    public: list[str] = Field(default_factory=list)


class ManifestEntitlementsSection(BaseModel):
    plan: str
    limits: dict[str, Any] = Field(default_factory=dict)
    usage_this_period: dict[str, Any] = Field(default_factory=dict)


class ManifestBffConventions(BaseModel):
    catalog_version: str = "2.0.0"
    base_path: str = "/api"
    ports: list[dict[str, Any]] = Field(default_factory=list)
    records: dict[str, Any] = Field(default_factory=dict)
    auth_users: dict[str, Any] = Field(default_factory=dict)


class ManifestPlatformSDK(BaseModel):
    version: str = "1.0.0"
    available: dict[str, Any] = Field(default_factory=dict)


class ManifestQueryDslSection(BaseModel):
    """Record engine query DSL reference for codegen.

    Describes the full query surface of the platform record engine (query_dsl_v1).
    Generated apps query Cloud through the BFF. Customer databases are not an
    available generated-app store until customer-backend attach ships.
    """

    enabled: bool = True
    version: str = "1"

    # Filter operators, grouped by kind
    operators: dict[str, list[str]] = Field(default_factory=lambda: {
        "comparison": ["eq", "neq", "lt", "lte", "gt", "gte"],
        "array":      ["in", "notIn"],
        "string":     ["contains", "startsWith"],
        "null":       ["isNull", "isNotNull"],
        "range":      ["between"],
    })

    # Boolean logic combinators
    logic: list[str] = Field(default_factory=lambda: ["AND", "OR", "NOT"])

    # Relation loading
    relations: dict[str, str] = Field(default_factory=lambda: {
        "include":           "eager-load related records alongside the query result",
        "filter_by_related": "apply conditions on related record fields in the WHERE clause",
    })

    # Aggregation operations
    aggregations: list[str] = Field(
        default_factory=lambda: ["count", "sum", "avg", "min", "max", "group_by"]
    )

    # Bulk mutation operations (operate on sets matched by a filter)
    bulk_operations: list[str] = Field(
        default_factory=lambda: ["updateWhere", "deleteWhere"]
    )

    # Sorting and pagination
    sorting: str = "multi-field, asc/desc per field"
    pagination: list[str] = Field(default_factory=lambda: ["limit_offset", "cursor"])

    # D1 scale ceiling — codegen must respect this
    d1_ceiling: dict[str, Any] = Field(default_factory=lambda: {
        "optimal_max_rows": 500_000,
        "hard_ceiling_note": (
            "The Cloud record engine is the only application store for generated apps. "
            "It works well up to ~500k rows. Do not create a local database or attach a "
            "customer origin. Customer-backend attach is a later platform feature."
        ),
        "use_external_connections_when": [],
    })

    # Decision guidance for codegen AI
    guidance: str = (
        "If no customer backend is provided, use the Cloud record engine via the BFF "
        "/api/records adapter and this query DSL. Customer-backend attach is a later "
        "feature; most generated apps stay on P2HS Cloud. Do not generate Hyperdrive "
        "clients, local SQLite/KV, or a second store."
    )


class ManifestDataImportSection(BaseModel):
    """Installed import session ports. Imports are HTTP sessions, not queue jobs."""

    enabled: bool = False
    async_import: dict[str, Any] = Field(default_factory=dict)
    batch_write: dict[str, Any] = Field(default_factory=dict)
    pre_generation_question: str = (
        "Use installed storage ports to upload a file, then installed import session ports. "
        "Poll the import session port; do not submit data.import as a queue request type. "
        "Application data remains in Cloud records. Do not invent a local database."
    )


class ManifestEmailTemplatesSection(BaseModel):
    """Every customizable transactional/pack email. Cloud owns runtime/default copy;
    this catalog is the Core validation mirror. current is live D1 overrides (absent
    key = platform default, not broken). Overlay file is replace-all desired state."""

    catalog: dict[str, Any] = Field(default_factory=dict)
    current: list[dict[str, Any]] = Field(default_factory=list)
    pre_generation_question: str = (
        "To customize copy, write p2hs/email-templates.json as the complete override set "
        "(replace-all). Keys omitted from the file revert to the platform default; they are "
        "not retained. Omit the file entirely to leave Cloud state unchanged. An empty "
        "templates array clears all overrides. Every protected_placeholder listed in "
        "catalog[key] must still appear literally in source text_body (and html_body, if set) "
        "or Core rejects the whole file before Cloud is mutated. enabled:false on an "
        "optional (contact_capture.*) override means ignore that override and use the platform "
        "default — it does not skip sending. optional:false (auth.*) ignores enabled:false. "
        "Runtime variables in html_body are HTML-escaped at Cloud render; subject and "
        "text_body are not. Pass raw strings, not HTML fragments. Overrides may reference only "
        "the catalog variable vocabulary for that template_key; unknown {{placeholders}} are rejected."
    )


class ManifestResponse(BaseModel):
    generated_at: datetime
    project: ManifestProjectSection
    credentials: ManifestCredentialsSection
    backend: ManifestBackendSection = Field(default_factory=ManifestBackendSection)
    env_vars: ManifestEnvVarsSection
    database: ManifestDatabaseSection
    auth: ManifestAuthSection
    storage: ManifestStorageSection
    functions: ManifestFunctionsSection
    feature_packs: ManifestFeaturePacksSection
    connectors: dict[str, Any]
    integrations: dict[str, Any]
    connections: dict[str, Any]
    realtime: ManifestRealtimeSection
    ai: ManifestAISection
    schedules: list[ManifestSchedule]
    knowledge_base: ManifestKnowledgeBaseSection
    api_surface: dict[str, Any]
    bff_conventions: ManifestBffConventions
    platform_sdk: ManifestPlatformSDK
    entitlements: ManifestEntitlementsSection
    query_dsl: ManifestQueryDslSection
    data_import: ManifestDataImportSection
    email_templates: ManifestEmailTemplatesSection = Field(default_factory=ManifestEmailTemplatesSection)


class ManifestDiffResponse(BaseModel):
    since_run_id: str
    generated_at: datetime
    changed_sections: list[str]
    sections: dict[str, Any]


# ── Codegen Run ──────────────────────────────────────────────────────────────

class StartRunRequest(BaseModel):
    requested_scopes: Optional[dict[str, Any]] = None


class RunTokenResponse(BaseModel):
    run_id: str
    token: str
    expires_at: datetime
    manifest: ManifestResponse


class CompleteRunRequest(BaseModel):
    manifest_snapshot_hash: Optional[str] = None


class FailRunRequest(BaseModel):
    reason: str


class ManifestRunResponse(BaseModel):
    id: UUID
    project_id: UUID
    initiated_by: Optional[UUID]
    status: str
    requested_scopes: Optional[dict[str, Any]]
    expires_at: datetime
    completed_at: Optional[datetime]
    failed_reason: Optional[str]
    manifest_snapshot_hash: Optional[str]
    created_at: datetime

    model_config = {"from_attributes": True}
