"""
Base Connector

Abstract base class for all database connectors. Defines the interface
that all connectors must implement.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any

from pydantic import BaseModel, Field

from ..exceptions import ValidationError
from ..models import DataModel


class ConnectorTableColumn(BaseModel):
    """Column definition for a connector table."""

    name: str = Field(..., description="Column name")
    type: str | None = Field(None, description="Column type (e.g., 'integer', 'string', 'decimal')")
    description: str | None = Field(None, description="Column description for AI context")


class ConnectorTable(BaseModel):
    """Table definition for a connector."""

    name: str = Field(..., description="Table name")
    description: str | None = Field(None, description="Table description for AI context")
    columns: list[ConnectorTableColumn] | None = Field(
        None, description="Column definitions (auto-discovered if not provided)"
    )
    data_model: DataModel | None = Field(
        None, description="Pre-applied constraints (filters, etc.)"
    )


class ConnectorRelationship(BaseModel):
    """Relationship (JOIN) between two connector tables.

    Defines how tables relate to each other so the Annie API can generate
    cross-table queries with proper JOINs.

    Example:
        ConnectorRelationship(
            primary_table="orders",
            primary_column="product_id",
            foreign_table="products",
            foreign_column="id",
            description="Each order references a product",
        )
    """

    primary_table: str = Field(..., description="Primary (left) table name")
    primary_column: str = Field(..., description="Column in the primary table (e.g., foreign key)")
    foreign_table: str = Field(..., description="Foreign (right) table name")
    foreign_column: str = Field(..., description="Column in the foreign table (e.g., primary key)")
    description: str | None = Field(None, description="Relationship description for AI context")


class BaseConnector(ABC):
    """Abstract base class for database connectors.

    Connectors handle database connections and SQL execution.
    They provide schema context for the Annie API and execute
    generated SQL locally.
    """

    def __init__(
        self,
        tables: list[ConnectorTable] | None = None,
        relationships: list[ConnectorRelationship] | None = None,
    ):
        self._tables = tables or []
        self._relationships = relationships or []
        self._connected = False
        self._columns_cache: dict[str, list[ConnectorTableColumn]] = {}

        if not self._tables:
            raise ValidationError("At least one table must be configured", field="tables")

    # =========================================================================
    # Abstract methods
    # =========================================================================

    @abstractmethod
    def connect(self) -> None:
        """Establish connection to the database."""
        ...

    @abstractmethod
    def disconnect(self) -> None:
        """Close the database connection."""
        ...

    @abstractmethod
    def execute(self, sql: str) -> list[dict[str, Any]]:
        """Execute SQL and return results as list of dicts."""
        ...

    @abstractmethod
    def discover_tables(self) -> list[str]:
        """List available tables in the database."""
        ...

    @abstractmethod
    def discover_columns(self, table_name: str) -> list[ConnectorTableColumn]:
        """Discover columns for a specific table."""
        ...

    @property
    @abstractmethod
    def dialect(self) -> str:
        """SQL dialect identifier (e.g., 'postgres', 'mysql', 'bigquery')."""
        ...

    # =========================================================================
    # Common methods
    # =========================================================================

    @property
    def tables(self) -> list[ConnectorTable]:
        """Get configured tables."""
        return self._tables

    @property
    def relationships(self) -> list[ConnectorRelationship]:
        """Get configured relationships."""
        return self._relationships

    def is_connected(self) -> bool:
        """Check if the connector is currently connected."""
        return self._connected

    def get_table_config(self, table_name: str) -> ConnectorTable | None:
        """Get configuration for a specific table."""
        for table in self._tables:
            if table.name == table_name:
                return table
        return None

    def get_table_schema(self, table_name: str) -> list[ConnectorTableColumn]:
        """Get schema for a table (from config, cache, or discovered)."""
        table = self.get_table_config(table_name)
        if table and table.columns:
            return table.columns

        # Return from cache if available
        if table_name in self._columns_cache:
            return self._columns_cache[table_name]

        # Auto-discover: connect if needed, then cache
        return self._discover_and_cache(table_name)

    def get_schema_context(self) -> dict[str, Any]:
        """Build schema context for the Annie API.

        Returns the schema in the format expected by the /sdk/query endpoint.
        Auto-discovers columns from the database for tables that don't have
        them explicitly defined. Discovered columns are cached.
        """
        tables = []
        for table in self._tables:
            if table.columns:
                # User-provided columns always take priority
                columns = [col.model_dump(exclude_none=True) for col in table.columns]
            else:
                # Auto-discover and cache
                discovered = self._discover_and_cache(table.name)
                columns = [col.model_dump(exclude_none=True) for col in discovered]

            table_dict: dict[str, Any] = {
                "name": table.name,
                "columns": columns,
            }
            if table.description:
                table_dict["description"] = table.description
            if table.data_model:
                table_dict["constraints"] = table.data_model.model_dump()

            tables.append(table_dict)

        result: dict[str, Any] = {"tables": tables}

        if self._relationships:
            result["relationships"] = [
                rel.model_dump(exclude_none=True) for rel in self._relationships
            ]

        return result

    def _discover_and_cache(self, table_name: str) -> list[ConnectorTableColumn]:
        """Discover columns for a table, caching the result.

        Connects to the database if not already connected.
        """
        if table_name in self._columns_cache:
            return self._columns_cache[table_name]

        if not self.is_connected():
            self.connect()

        discovered = self.discover_columns(table_name)
        self._columns_cache[table_name] = discovered
        return discovered

    # =========================================================================
    # Context manager
    # =========================================================================

    def __enter__(self) -> BaseConnector:
        self.connect()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        self.disconnect()
