from __future__ import annotations

from collections.abc import Callable
from datetime import date, datetime, timezone
from decimal import Decimal
from typing import Any

from .config import Settings

ConnectionFactory = Callable[[], Any]
ConnectorType = Callable[..., Any]
DATABASE_CONNECT_TIMEOUT_SECONDS = 8


class ReaderContractError(RuntimeError):
    """The live database session does not match the adapter's read-only contract."""


def _json_value(value: Any) -> Any:
    if isinstance(value, Decimal):
        return float(value)
    if isinstance(value, (date, datetime)):
        return value.isoformat()
    if isinstance(value, tuple):
        return list(value)
    return value


def _column_name(column: Any) -> str:
    return str(column.name) if hasattr(column, "name") else str(column[0])


def _row_dict(cursor: Any, row: Any) -> dict[str, Any] | None:
    if row is None:
        return None
    names = [_column_name(column) for column in cursor.description]
    return {name: _json_value(value) for name, value in zip(names, row, strict=True)}


def _rows(cursor: Any) -> list[dict[str, Any]]:
    names = [_column_name(column) for column in cursor.description]
    return [
        {name: _json_value(value) for name, value in zip(names, row, strict=True)}
        for row in cursor.fetchall()
    ]


def _private_connection_factory(
    settings: Settings,
    *,
    connector_type: ConnectorType,
    private_ip: Any,
) -> tuple[Any, ConnectionFactory]:
    connector = connector_type(
        refresh_strategy="LAZY",
        timeout=DATABASE_CONNECT_TIMEOUT_SECONDS,
    )

    def connect() -> Any:
        return connector.connect(
            settings.instance_connection_name,
            "pg8000",
            user=settings.iam_database_user,
            db=settings.database,
            enable_iam_auth=True,
            ip_type=private_ip,
            timeout=DATABASE_CONNECT_TIMEOUT_SECONDS,
        )

    return connector, connect


class CloudSqlClaimReader:
    def __init__(self, settings: Settings, *, connection_factory: ConnectionFactory | None = None):
        self.settings = settings
        self._connector: Any | None = None
        if connection_factory is not None:
            self._connection_factory = connection_factory
            return

        from google.cloud.sql.connector import Connector, IPTypes

        self._connector, self._connection_factory = _private_connection_factory(
            settings,
            connector_type=Connector,
            private_ip=IPTypes.PRIVATE,
        )

    def close(self) -> None:
        if self._connector is not None:
            self._connector.close()

    def check_ready(self) -> dict[str, Any]:
        connection = self._connection_factory()
        try:
            cursor = connection.cursor()
            self._begin_read_only(cursor)
            session = self._load_session_contract(cursor)
            connection.rollback()
            return {
                "ready": True,
                "database": session["database"],
                "authorization_role": self.settings.authorization_role,
                "transaction_read_only": True,
            }
        finally:
            connection.close()

    def read_claim(self, claim_id: str | None = None) -> dict[str, Any] | None:
        connection = self._connection_factory()
        try:
            cursor = connection.cursor()
            self._begin_read_only(cursor)
            session = self._load_session_contract(cursor)
            claim = self._load_claim(cursor, claim_id)
            if claim is None:
                connection.rollback()
                return None

            evidence = self._load_evidence(cursor, claim["id"])
            edges = self._load_edges(cursor, claim["id"])
            proposal_counts = self._load_proposal_counts(cursor)
            related_proposals = self._load_related_proposals(cursor, claim["id"])
            recent_proposals = self._load_recent_proposals(cursor)
            connection.rollback()
        finally:
            connection.close()

        generated_at = datetime.now(timezone.utc).isoformat()
        return {
            "schema": "livingip.observatory-canonical-claim.v1",
            "generated_at": generated_at,
            "read_only": True,
            "provenance": {
                "gcp_project": self.settings.project_id,
                "cloud_sql_instance": self.settings.instance_connection_name,
                "database": session["database"],
                "database_principal": session["database_principal"],
                "authorization_role": self.settings.authorization_role,
                "transaction_read_only": True,
                "write_privileges_denied": bool(session["required_writes_denied"]),
                "service_revision": self.settings.service_revision,
            },
            "canonical": {
                "relation": "public.claims",
                "claim": claim,
                "evidence_relation": "public.claim_evidence -> public.sources",
                "evidence": evidence,
                "edge_relation": "public.claim_edges",
                "edges": edges,
            },
            "proposal_ledger": {
                "relation": "kb_stage.kb_proposals",
                "distinct_from_canonical": True,
                "status_counts": proposal_counts,
                "related": related_proposals,
                "recent": recent_proposals,
                "semantics": {
                    "pending_review": "staged only; not canonical",
                    "approved": "reviewed intent; not canonical until applied_at is set",
                    "applied": "proposal receipt only; canonical rows remain authoritative",
                },
            },
        }

    @staticmethod
    def _begin_read_only(cursor: Any) -> None:
        cursor.execute("set transaction read only")
        cursor.execute("set local statement_timeout = '5000ms'")
        cursor.execute("set local lock_timeout = '1000ms'")

    def _load_session_contract(self, cursor: Any) -> dict[str, Any]:
        cursor.execute(
            """
            select current_database() as database,
                   current_user as database_principal,
                   current_setting('transaction_read_only') as transaction_read_only,
                   pg_has_role(current_user, %s, 'member') as has_authorization_role,
                   has_table_privilege(current_user, 'public.claims', 'select')
                     and has_table_privilege(current_user, 'public.sources', 'select')
                     and has_table_privilege(current_user, 'public.claim_evidence', 'select')
                     and has_table_privilege(current_user, 'public.claim_edges', 'select')
                     and has_table_privilege(current_user, 'kb_stage.kb_proposals', 'select')
                     as has_required_reads,
                   not (
                     has_table_privilege(current_user, 'public.claims', 'insert,update,delete,truncate')
                     or has_table_privilege(current_user, 'public.sources', 'insert,update,delete,truncate')
                     or has_table_privilege(current_user, 'public.claim_evidence', 'insert,update,delete,truncate')
                     or has_table_privilege(current_user, 'public.claim_edges', 'insert,update,delete,truncate')
                     or has_table_privilege(current_user, 'kb_stage.kb_proposals', 'insert,update,delete,truncate')
                   ) as required_writes_denied
            """,
            (self.settings.authorization_role,),
        )
        session = _row_dict(cursor, cursor.fetchone())
        if session is None:
            raise ReaderContractError("database session contract returned no row")
        if session["database"] != self.settings.database:
            raise ReaderContractError("database session selected an unexpected database")
        if session["database_principal"] != self.settings.iam_database_user:
            raise ReaderContractError("database session selected an unexpected principal")
        if session["transaction_read_only"] != "on":
            raise ReaderContractError("database session is not read-only")
        if not session["has_authorization_role"] or not session["has_required_reads"]:
            raise ReaderContractError("database principal lacks the exact read authorization")
        if not session["required_writes_denied"]:
            raise ReaderContractError("database principal has an unexpected write privilege")
        return session

    @staticmethod
    def _load_claim(cursor: Any, claim_id: str | None) -> dict[str, Any] | None:
        columns = """
            select c.id::text as id,
                   c.type::text as type,
                   left(c.text, 12000) as text,
                   c.status::text as status,
                   c.confidence,
                   coalesce(c.tags, '{}'::text[]) as tags,
                   c.superseded_by::text as superseded_by,
                   c.created_at,
                   c.updated_at
              from public.claims c
        """
        if claim_id is not None:
            cursor.execute(columns + " where c.id = %s::uuid", (claim_id,))
            return _row_dict(cursor, cursor.fetchone())

        cursor.execute(
            columns
            + """
             where exists (
               select 1 from public.claim_evidence ce where ce.claim_id = c.id
             )
             order by coalesce(c.updated_at, c.created_at) desc, c.id desc
             limit 1
            """
        )
        claim = _row_dict(cursor, cursor.fetchone())
        if claim is not None:
            return claim
        cursor.execute(columns + " order by coalesce(c.updated_at, c.created_at) desc, c.id desc limit 1")
        return _row_dict(cursor, cursor.fetchone())

    @staticmethod
    def _load_evidence(cursor: Any, claim_id: str) -> list[dict[str, Any]]:
        cursor.execute(
            """
            select ce.id::text as evidence_id,
                   ce.role::text as role,
                   ce.weight,
                   ce.created_at as linked_at,
                   s.id::text as source_id,
                   s.source_type::text as source_type,
                   s.url,
                   s.storage_path,
                   left(coalesce(s.excerpt, ''), 2400) as excerpt,
                   s.hash as content_hash,
                   s.captured_at,
                   s.created_at as source_created_at
              from public.claim_evidence ce
              join public.sources s on s.id = ce.source_id
             where ce.claim_id = %s::uuid
             order by ce.weight desc nulls last, ce.created_at, ce.id
             limit 30
            """,
            (claim_id,),
        )
        return _rows(cursor)

    @staticmethod
    def _load_edges(cursor: Any, claim_id: str) -> list[dict[str, Any]]:
        cursor.execute(
            """
            select e.id::text as edge_id,
                   case when e.from_claim = %s::uuid then 'outgoing' else 'incoming' end as direction,
                   e.edge_type::text as edge_type,
                   e.weight,
                   case when e.from_claim = %s::uuid then e.to_claim::text else e.from_claim::text end
                     as connected_claim_id,
                   e.created_at
              from public.claim_edges e
             where e.from_claim = %s::uuid or e.to_claim = %s::uuid
             order by e.created_at, e.id
             limit 40
            """,
            (claim_id, claim_id, claim_id, claim_id),
        )
        return _rows(cursor)

    @staticmethod
    def _load_proposal_counts(cursor: Any) -> dict[str, int]:
        cursor.execute(
            "select status::text as status, count(*)::int as count "
            "from kb_stage.kb_proposals group by status order by status"
        )
        return {str(status): int(count) for status, count in cursor.fetchall()}

    @staticmethod
    def _load_related_proposals(cursor: Any, claim_id: str) -> list[dict[str, Any]]:
        cursor.execute(
            """
            select p.id::text as id,
                   p.proposal_type::text as proposal_type,
                   p.status::text as status,
                   to_jsonb(p)->>'source_ref' as source_ref,
                   p.reviewed_at,
                   p.applied_at,
                   p.updated_at
              from kb_stage.kb_proposals p
             where p.payload::text like %s
             order by p.updated_at desc, p.id desc
             limit 20
            """,
            (f"%{claim_id}%",),
        )
        return _rows(cursor)

    @staticmethod
    def _load_recent_proposals(cursor: Any) -> list[dict[str, Any]]:
        cursor.execute(
            """
            select p.id::text as id,
                   p.proposal_type::text as proposal_type,
                   p.status::text as status,
                   to_jsonb(p)->>'source_ref' as source_ref,
                   p.reviewed_at,
                   p.applied_at,
                   p.updated_at
              from kb_stage.kb_proposals p
             order by p.updated_at desc, p.id desc
             limit 10
            """
        )
        return _rows(cursor)
