"""PostgreSQL database layer using asyncpg.

Provides a PgDatabase wrapper that mimics the aiosqlite interface
(execute_fetchall, execute, commit) so route handlers need minimal changes.
"""

from __future__ import annotations

import logging
import os
import re
from contextlib import asynccontextmanager

import asyncpg

logger = logging.getLogger(__name__)

# Module-level pool reference (set by init_db, read by get_pool)
_pool: asyncpg.Pool | None = None


def get_database_url() -> str:
    """Return the PostgreSQL connection URL from DATABASE_URL env var."""
    url = os.environ.get("DATABASE_URL", "")
    if not url:
        raise RuntimeError(
            "DATABASE_URL environment variable is not set. "
            "Example: postgresql://user:pass@localhost:5432/peon"
        )
    return url


def get_pool() -> asyncpg.Pool:
    """Return the module-level connection pool.

    Used by github_webhooks handlers and discord gateway that need
    direct pool access outside the FastAPI dependency system.
    """
    if _pool is None:
        raise RuntimeError("Database pool not initialized. Call init_db() first.")
    return _pool


def _convert_params(sql: str, params: tuple | list | None) -> tuple[str, list]:
    """Convert SQLite-style ? placeholders to PostgreSQL $1, $2, ... style.

    Returns (converted_sql, params_list).
    """
    if params is None:
        params = []
    else:
        params = list(params)

    if "?" not in sql:
        return sql, params

    counter = 0
    parts = []
    i = 0
    while i < len(sql):
        if sql[i] == "?":
            counter += 1
            parts.append(f"${counter}")
        elif sql[i] == "'":
            # Skip string literals
            j = i + 1
            while j < len(sql) and sql[j] != "'":
                if sql[j] == "'" and j + 1 < len(sql) and sql[j + 1] == "'":
                    j += 2
                else:
                    j += 1
            parts.append(sql[i : j + 1])
            i = j
        else:
            parts.append(sql[i])
        i += 1

    return "".join(parts), params


class PgCursorResult:
    """Mimics aiosqlite cursor result with lastrowid and rowcount."""

    def __init__(self, lastrowid: int | None = None, rowcount: int = 0):
        self.lastrowid = lastrowid
        self.rowcount = rowcount

    @classmethod
    def from_status(cls, status: str, returning_row: asyncpg.Record | None = None) -> "PgCursorResult":
        """Build from asyncpg status string and optional RETURNING row."""
        lastrowid = None
        rowcount = 0

        if returning_row is not None:
            # RETURNING id clause was used
            lastrowid = returning_row.get("id") if hasattr(returning_row, "get") else returning_row[0]

        # Parse rowcount from status strings like "UPDATE 3", "DELETE 1", "INSERT 0 1"
        if status:
            parts = status.split()
            if parts[0] == "INSERT" and len(parts) >= 3:
                rowcount = int(parts[2])
            elif parts[0] in ("UPDATE", "DELETE") and len(parts) >= 2:
                rowcount = int(parts[1])

        return cls(lastrowid=lastrowid, rowcount=rowcount)


class _PgTransaction:
    """Single-connection wrapper used inside ``PgDatabase.transaction()``.

    Provides the same query API as ``PgDatabase`` but all operations share
    one connection and transaction, enabling ``SELECT … FOR UPDATE`` and
    multi-statement atomicity.
    """

    def __init__(self, conn: asyncpg.Connection):
        self._conn = conn

    async def execute_fetchall(self, sql: str, params: tuple | list | None = None) -> list[dict]:
        converted_sql, converted_params = _convert_params(sql, params)
        rows = await self._conn.fetch(converted_sql, *converted_params)
        return [dict(r) for r in rows]

    async def execute_fetchone(self, sql: str, params: tuple | list | None = None) -> dict | None:
        converted_sql, converted_params = _convert_params(sql, params)
        row = await self._conn.fetchrow(converted_sql, *converted_params)
        return dict(row) if row else None

    async def execute(self, sql: str, params: tuple | list | None = None) -> PgCursorResult:
        converted_sql, converted_params = _convert_params(sql, params)
        upper_sql = converted_sql.strip().upper()
        if "RETURNING" in upper_sql:
            row = await self._conn.fetchrow(converted_sql, *converted_params)
            return PgCursorResult(
                lastrowid=row["id"] if row and "id" in row else None,
                rowcount=1 if row else 0,
            )
        else:
            status = await self._conn.execute(converted_sql, *converted_params)
            return PgCursorResult.from_status(status)

    async def commit(self) -> None:
        """No-op — transaction commits when the context manager exits."""
        pass


class PgDatabase:
    """Wrapper around asyncpg.Pool that mimics the aiosqlite interface."""

    def __init__(self, pool: asyncpg.Pool):
        self._pool = pool

    @asynccontextmanager
    async def transaction(self):
        """Yield a single-connection wrapper within an explicit transaction.

        All operations inside the block share one connection and transaction,
        enabling ``SELECT … FOR UPDATE`` and multi-statement atomicity.
        Commits on normal exit, rolls back on exception.
        """
        async with self._pool.acquire() as conn:
            async with conn.transaction():
                yield _PgTransaction(conn)

    async def execute_fetchall(self, sql: str, params: tuple | list | None = None) -> list[dict]:
        """Execute a query and return all rows as list of dicts."""
        converted_sql, converted_params = _convert_params(sql, params)
        async with self._pool.acquire() as conn:
            rows = await conn.fetch(converted_sql, *converted_params)
            return [dict(r) for r in rows]

    async def execute_fetchone(self, sql: str, params: tuple | list | None = None) -> dict | None:
        """Execute a query and return a single row as dict, or None."""
        converted_sql, converted_params = _convert_params(sql, params)
        async with self._pool.acquire() as conn:
            row = await conn.fetchrow(converted_sql, *converted_params)
            return dict(row) if row else None

    async def execute(self, sql: str, params: tuple | list | None = None) -> PgCursorResult:
        """Execute a statement and return a cursor-like result."""
        converted_sql, converted_params = _convert_params(sql, params)
        async with self._pool.acquire() as conn:
            # Check if this is an INSERT/UPDATE/DELETE that might need RETURNING
            upper_sql = converted_sql.strip().upper()

            if "RETURNING" in upper_sql:
                row = await conn.fetchrow(converted_sql, *converted_params)
                return PgCursorResult(
                    lastrowid=row["id"] if row and "id" in row else None,
                    rowcount=1 if row else 0,
                )
            else:
                status = await conn.execute(converted_sql, *converted_params)
                return PgCursorResult.from_status(status)

    async def commit(self) -> None:
        """No-op — asyncpg auto-commits each statement."""
        pass

    async def close(self) -> None:
        """Close the underlying connection pool."""
        await self._pool.close()


def row_to_dict(row) -> dict:
    """Convert an asyncpg.Record or dict to a plain dict."""
    if isinstance(row, dict):
        return row
    return dict(row)


def _run_alembic_migrations(database_url: str) -> None:
    """Run Alembic migrations programmatically."""
    from pathlib import Path

    from alembic import command
    from alembic.config import Config

    migrations_dir = str(Path(__file__).parent / "migrations")

    cfg = Config()
    cfg.set_main_option("script_location", migrations_dir)
    cfg.set_main_option("sqlalchemy.url", database_url)

    command.upgrade(cfg, "head")


async def init_db(database_url: str | None = None) -> PgDatabase:
    """Initialize the database connection pool and run migrations.

    Args:
        database_url: Optional PostgreSQL connection URL.
                      If not provided, uses DATABASE_URL env var.

    Returns:
        Configured PgDatabase wrapping an asyncpg connection pool.
    """
    global _pool

    url = database_url or get_database_url()

    # Convert asyncpg-style URL to SQLAlchemy-style for Alembic
    sa_url = url
    if sa_url.startswith("postgres://"):
        sa_url = "postgresql://" + sa_url[len("postgres://"):]
    elif sa_url.startswith("postgresql://"):
        pass  # already correct for SQLAlchemy
    # For psycopg driver (Alembic sync), ensure we use postgresql+psycopg
    if "://" in sa_url and "+psycopg" not in sa_url:
        sa_url = sa_url.replace("postgresql://", "postgresql+psycopg://", 1)

    # Run Alembic migrations (uses SQLAlchemy sync connection)
    _run_alembic_migrations(sa_url)

    # Create asyncpg connection pool
    # asyncpg needs postgresql:// or postgres:// URLs
    asyncpg_url = url
    if asyncpg_url.startswith("postgresql+psycopg://"):
        asyncpg_url = asyncpg_url.replace("postgresql+psycopg://", "postgresql://", 1)

    _pool = await asyncpg.create_pool(asyncpg_url, min_size=2, max_size=20)

    return PgDatabase(_pool)
