"""
PostgreSQL Connector

Connects to PostgreSQL databases for query execution.
"""

from __future__ import annotations

from typing import Any

from ..exceptions import ConnectionError, ValidationError
from .base import BaseConnector, ConnectorRelationship, ConnectorTable, ConnectorTableColumn

# Type mapping from PostgreSQL to SDK types
PG_TYPE_MAP = {
    "int2": "integer",
    "int4": "integer",
    "int8": "integer",
    "smallint": "integer",
    "integer": "integer",
    "bigint": "integer",
    "serial": "integer",
    "bigserial": "integer",
    "float4": "float",
    "float8": "float",
    "real": "float",
    "double precision": "float",
    "numeric": "decimal",
    "decimal": "decimal",
    "money": "decimal",
    "bool": "boolean",
    "boolean": "boolean",
    "varchar": "string",
    "character varying": "string",
    "char": "string",
    "character": "string",
    "text": "string",
    "name": "string",
    "uuid": "string",
    "json": "string",
    "jsonb": "string",
    "date": "date",
    "time": "time",
    "timetz": "time",
    "timestamp": "timestamp",
    "timestamptz": "timestamp",
    "timestamp without time zone": "timestamp",
    "timestamp with time zone": "timestamp",
    "interval": "string",
    "bytea": "binary",
    "inet": "string",
    "cidr": "string",
    "macaddr": "string",
}


class PostgresConnector(BaseConnector):
    """PostgreSQL database connector.

    Connects to a PostgreSQL database using psycopg2. Supports both
    connection string and individual parameters.

    Args:
        connection_string: PostgreSQL connection string (postgresql://user:pass@host:port/db)
        host: Database host
        port: Database port (default: 5432)
        database: Database name
        user: Database user
        password: Database password
        schema: Database schema (default: 'public')
        tables: List of table configurations
    """

    def __init__(
        self,
        connection_string: str | None = None,
        host: str | None = None,
        port: int = 5432,
        database: str | None = None,
        user: str | None = None,
        password: str | None = None,
        schema: str = "public",
        tables: list[ConnectorTable] | None = None,
        relationships: list[ConnectorRelationship] | None = None,
    ):
        super().__init__(tables=tables, relationships=relationships)

        self._connection_string = connection_string
        self._host = host
        self._port = port
        self._database = database
        self._user = user
        self._password = password
        self._schema = schema
        self._conn = None

        if not connection_string and not (host and database):
            raise ValidationError(
                "Either connection_string or host+database must be provided",
                field="connection",
            )

    @property
    def dialect(self) -> str:
        return "postgres"

    def connect(self) -> None:
        """Establish connection to PostgreSQL."""
        try:
            import psycopg2
        except ImportError as e:
            raise ConnectionError(
                "psycopg2 is required for PostgresConnector. "
                "Install it with: pip install psycopg2-binary",
                original_error=e,
            )

        try:
            if self._connection_string:
                self._conn = psycopg2.connect(self._connection_string)
            else:
                self._conn = psycopg2.connect(
                    host=self._host,
                    port=self._port,
                    database=self._database,
                    user=self._user,
                    password=self._password,
                )
            self._connected = True
        except Exception as e:
            raise ConnectionError(f"Failed to connect to PostgreSQL: {e}", original_error=e)

    def disconnect(self) -> None:
        """Close the PostgreSQL connection."""
        if self._conn:
            try:
                self._conn.close()
            except Exception:
                pass
        self._conn = None
        self._connected = False

    def execute(self, sql: str) -> list[dict[str, Any]]:
        """Execute SQL against PostgreSQL and return results."""
        if not self._conn:
            self.connect()

        try:
            cursor = self._conn.cursor()
            cursor.execute(sql)

            if cursor.description is None:
                return []

            columns = [desc[0] for desc in cursor.description]
            rows = cursor.fetchall()

            return [
                {col: self._convert_value(val) for col, val in zip(columns, row)}
                for row in rows
            ]
        except Exception as e:
            # Rollback the transaction to keep the connection usable
            try:
                self._conn.rollback()
            except Exception:
                pass
            raise ConnectionError(f"Query execution failed: {e}", original_error=e)

    def discover_tables(self) -> list[str]:
        """List all tables in the configured schema."""
        if not self._conn:
            self.connect()

        result = self.execute(
            f"SELECT table_name FROM information_schema.tables "
            f"WHERE table_schema = '{self._schema}' "
            f"ORDER BY table_name"
        )
        return [row["table_name"] for row in result]

    def discover_columns(self, table_name: str) -> list[ConnectorTableColumn]:
        """Discover columns for a table."""
        if not self._conn:
            self.connect()

        result = self.execute(
            f"SELECT column_name, data_type "
            f"FROM information_schema.columns "
            f"WHERE table_schema = '{self._schema}' "
            f"AND table_name = '{table_name}' "
            f"ORDER BY ordinal_position"
        )

        return [
            ConnectorTableColumn(
                name=row["column_name"],
                type=PG_TYPE_MAP.get(row["data_type"], row["data_type"]),
            )
            for row in result
        ]

    @staticmethod
    def _convert_value(val: Any) -> Any:
        """Convert database values to JSON-serializable types."""
        if val is None:
            return None
        # Handle Decimal
        if hasattr(val, "as_tuple"):
            return float(val)
        # Handle datetime
        if hasattr(val, "isoformat"):
            return val.isoformat()
        # Handle memoryview / bytes
        if isinstance(val, (bytes, memoryview)):
            return str(val)
        return val
