"""Tables DATA des intégrations BYOK et de leur planification (spec Imports §1.11, T8).

Deux tables, une migration (§9) :

- `integrations` — **une connexion par (organisation, fournisseur)**. Elle porte le secret du
  client, chiffré par `app.core.crypto` : la colonne `credentials` ne contient jamais la clé
  en clair, et rien dans l'api ne la renvoie — l'écran n'affiche que `credentials_hint`, les
  quatre derniers caractères, stockés en clair *exprès* pour que la page se rende sans
  déchiffrer quoi que ce soit.
- `import_schedules` — **le mode de synchro par (intégration, cible)**, décision utilisateur
  2026-08-31 : `manual` (défaut, bouton « Importer maintenant ») ou récurrent (`weekly` /
  `monthly`). `next_run_at` est le rendez-vous que le sweep du worker lit ; il est **NULL en
  mode `manual`** — une contrainte le garantit, sans quoi un mode remis à la main laisserait
  une échéance fantôme que le sweep honorerait quand même.

Invariants transverses (architecture §8) : `org_id TEXT NOT NULL` partout et filtré dans
chaque requête, PK `uuidv7()`, horodatages `timestamptz`.
"""

import uuid
from datetime import datetime
from typing import Any

from sqlalchemy import (
    Boolean,
    CheckConstraint,
    DateTime,
    ForeignKey,
    Text,
    UniqueConstraint,
    func,
    text,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.db.base import Base

# Fournisseurs BYOK connus. Mangools d'abord (source préférée de l'utilisateur, §5) ;
# DataForSEO suivra sans migration — la contrainte liste, elle, en demandera une, et c'est
# voulu : brancher un fournisseur est une décision produit, pas une valeur de configuration.
PROVIDERS = ("mangools",)
# `unverified` : clé saisie, jamais confrontée au fournisseur. `connected` : le dernier appel
# à l'endpoint de quotas a répondu. `error` : il a refusé (clé invalide, quota, panne) —
# `status_error` porte le motif, en clair, jamais le secret.
INTEGRATION_STATUSES = ("unverified", "connected", "error")
# Décision utilisateur 2026-08-31 : deux cadences, pas de cron libre.
SYNC_MODES = ("manual", "weekly", "monthly")


class Integration(Base):
    """Une connexion BYOK d'une organisation à un fournisseur de données (§1.11)."""

    __tablename__ = "integrations"
    __mapper_args__ = {"eager_defaults": True}

    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), primary_key=True, server_default=text("uuidv7()")
    )
    # Pas d'index dédié : le btree de UNIQUE (org_id, provider) est préfixé par org_id.
    org_id: Mapped[str] = mapped_column(Text, nullable=False)
    provider: Mapped[str] = mapped_column(Text, nullable=False)
    # Jeton Fernet (`app.core.crypto.encrypt`), **jamais** le secret en clair. Aucune réponse
    # de l'api ne lit cette colonne ; seuls le job d'import et la vérification la déchiffrent.
    credentials: Mapped[str] = mapped_column(Text, nullable=False)
    # `••••••cd12` — de quoi reconnaître sa clé, pas de quoi la rejouer.
    credentials_hint: Mapped[str] = mapped_column(Text, nullable=False)
    # Réglages du fournisseur qui ne sont pas des secrets (identifiant de localisation par
    # défaut, par exemple). Jsonb pour ne pas migrer à chaque option d'un fournisseur.
    config: Mapped[dict[str, Any]] = mapped_column(
        JSONB, nullable=False, server_default=text("'{}'::jsonb")
    )
    status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'unverified'"))
    status_error: Mapped[str | None] = mapped_column(Text, nullable=True)
    checked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now()
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
    )

    schedules: Mapped[list["ImportSchedule"]] = relationship(
        back_populates="integration",
        cascade="all, delete-orphan",
        passive_deletes=True,
        order_by="ImportSchedule.id",
    )

    __table_args__ = (
        CheckConstraint(
            "provider IN ('mangools')",
            name="ck_integrations_provider",
        ),
        CheckConstraint(
            "status IN ('unverified', 'connected', 'error')", name="ck_integrations_status"
        ),
        # Une seule clé par fournisseur et par organisation : brancher deux comptes Mangools
        # sur la même org n'a pas de sens produit et rendrait « quelle clé a payé ce fetch »
        # indécidable.
        UniqueConstraint("org_id", "provider", name="uq_integrations_org_provider"),
    )


class ImportSchedule(Base):
    """Le mode de synchro d'une intégration **pour une cible** (§1.11).

    L'unicité `(integration_id, site_target_id)` est le cœur du modèle : une cible n'a qu'un
    mode par intégration, donc l'écran Intégrations affiche une ligne par cible et le sweep
    n'a jamais deux rendez-vous concurrents pour le même travail.
    """

    __tablename__ = "import_schedules"
    __mapper_args__ = {"eager_defaults": True}

    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), primary_key=True, server_default=text("uuidv7()")
    )
    org_id: Mapped[str] = mapped_column(Text, nullable=False, index=True)
    # Pas d'index dédié : le btree de UNIQUE (integration_id, site_target_id) est préfixé par
    # integration_id et sert le listing de l'écran.
    integration_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey(
            "integrations.id", ondelete="CASCADE", name="fk_import_schedules_integration_id"
        ),
        nullable=False,
    )
    # CASCADE : supprimer une cible emporte son planning, comme ses listes et ses imports
    # (décision n°4) — un rendez-vous sur une cible disparue n'a aucun sens.
    site_target_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey(
            "site_targets.id", ondelete="CASCADE", name="fk_import_schedules_site_target_id"
        ),
        nullable=False,
        index=True,
    )
    sync_mode: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'manual'"))
    # Le rendez-vous que le sweep lit. NULL en `manual` (contrainte ci-dessous).
    next_run_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
    # Interrupteur indépendant du mode : suspendre une synchro récurrente sans oublier sa
    # cadence, et la reprendre là où elle en était.
    enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))
    # Paramètres du fetch pour cette cible (domaine concurrent à interroger, identifiant de
    # localisation forcé…). Vide = l'adaptateur déduit tout de la cible et de son site.
    config: Mapped[dict[str, Any]] = mapped_column(
        JSONB, nullable=False, server_default=text("'{}'::jsonb")
    )
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now()
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
    )

    integration: Mapped[Integration] = relationship(back_populates="schedules")

    __table_args__ = (
        CheckConstraint(
            "sync_mode IN ('manual', 'weekly', 'monthly')", name="ck_import_schedules_sync_mode"
        ),
        # « Une échéance si et seulement si une cadence » : un `manual` qui garderait un
        # `next_run_at` serait balayé par le sweep alors que l'utilisateur a explicitement
        # demandé de ne rien lancer tout seul. La base l'interdit plutôt que le service.
        CheckConstraint(
            "(sync_mode = 'manual') = (next_run_at IS NULL)",
            name="ck_import_schedules_next_run_at",
        ),
        UniqueConstraint(
            "integration_id", "site_target_id", name="uq_import_schedules_integration_target"
        ),
    )
