import json
import logging
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urlparse, urlunparse

from sqlalchemy import select, text, and_, or_, literal_column
from sqlalchemy.sql import Select
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker

from .base import DBClient
from ..query import Query, WhereCondition, WhereGroup, QuerySet
from ..query.operators import Eq, Gt, Gte, Lt, Lte, In, Like, ILike, NotEq, IsNull, IsNotNull, RawExpr

logger = logging.getLogger(__name__)


class DorisClient(DBClient):
    def __init__(self, url: str, echo: bool = False):
        self.engine = create_async_engine(
            url,
            pool_size=10,
            max_overflow=5,
            pool_pre_ping=True,
            echo=echo,
            connect_args={
                "charset": "utf8mb4",
                "use_unicode": True,
            },
        )
        self.session_maker = async_sessionmaker(self.engine, expire_on_commit=False)

    async def connect(self):
        async with self.engine.connect() as connection:
            await connection.scalar(text("SELECT 1"))

    async def disconnect(self):
        await self.engine.dispose()

    def table(self, name: str) -> "QueryBuilder":
        from ..query import QueryBuilder
        return QueryBuilder(self, name)

    def raw_query(self, query: Query) -> Tuple[str, Dict[str, Any]]:
        stmt, params = self._build_select_statement(query, for_explain=True)
        compiled = stmt.compile(compile_kwargs={"literal_binds": True})
        sql_string = str(compiled)
        # Merge accumulated raw expression params
        if hasattr(query, '_params') and query._params:
            params = {**query._params, **params}
        return sql_string, params

    def debug_query(self, query: Query) -> str:
        sql, params = self.raw_query(query)
        debug_str = f"SQL: {sql}"
        if params:
            debug_str += f"\nParams: {params}"
        return debug_str

    async def execute_raw(
        self,
        sql: str,
        params: dict[str, Any] | None = None,
        *,
        multi_statement: bool = False,
    ) -> Any:
        """Execute arbitrary raw SQL against Apache Doris.

        Args:
            sql: Raw SQL string (may contain ``;``-delimited statements
                when ``multi_statement=True``).
            params: Optional parameter dict for bound parameters (``:name``).
            multi_statement: If True, the SQL may contain multiple statements
                separated by ``;``.  Uses a raw aiomysql connection to
                execute all statements sequentially.

        Returns:
            * ``list[dict[str, Any]]`` for SELECT-like statements.
            * ``int`` (rowcount) for INSERT/UPDATE/DELETE.
            * ``None`` for DDL like CREATE TABLE.
        """
        if multi_statement:
            # Use raw connection for multi-statement support
            async with self.engine.connect() as conn:
                raw_conn = await conn.get_raw_connection()
                cursor = await raw_conn.driver_connection.cursor()
                for stmt in sql.split(";"):
                    stmt = stmt.strip()
                    if stmt:
                        await cursor.execute(stmt)
                await cursor.close()
                await conn.commit()
            return None

        from sqlalchemy import text as sa_text

        async with self.session_maker() as session:
            result = await session.execute(sa_text(sql), params or {})
            if result.returns_rows:
                return [dict(row) for row in result.mappings()]
            await session.commit()
            return result.rowcount

    async def find_sequential_gaps(
        self,
        table: str,
        sequence_column: str,
        partition_column: str,
        partition_value: Any,
        *,
        min_sequence: int | None = None,
        max_sequence: int | None = None,
        extra_where: list[str] | None = None,
        extra_params: dict[str, Any] | None = None,
    ) -> list[dict[str, Any]]:
        """Find gaps in a sequential integer column using Doris ``LAG`` window."""
        where_clauses = [f"`{partition_column}` = :partition_value"]
        params: dict[str, Any] = {"partition_value": partition_value}

        if min_sequence is not None:
            where_clauses.append(f"`{sequence_column}` >= :min_seq")
            params["min_seq"] = min_sequence
        if max_sequence is not None:
            where_clauses.append(f"`{sequence_column}` <= :max_seq")
            params["max_seq"] = max_sequence
        if extra_where:
            where_clauses.extend(extra_where)
        if extra_params:
            params.update(extra_params)

        sql = f"""
            SELECT `{partition_column}`,
                   prev_id + 1 AS gap_start,
                   `{sequence_column}` - 1 AS gap_end
            FROM (
                SELECT `{partition_column}`,
                       `{sequence_column}`,
                       LAG(`{sequence_column}`) OVER (
                           PARTITION BY `{partition_column}`
                           ORDER BY `{sequence_column}`
                       ) AS prev_id
                FROM `{table}`
                WHERE {' AND '.join(where_clauses)}
            ) t
            WHERE `{sequence_column}` - prev_id > 1
            ORDER BY gap_start
        """

        return await self.execute_raw(sql, params)

    async def explain(self, query: Query, analyze: bool = False) -> str:
        sql, _ = self.raw_query(query)
        explain_clause = "EXPLAIN ANALYZE" if analyze else "EXPLAIN"
        explain_sql = f"{explain_clause} {sql}"

        async with self.session_maker() as session:
            result = await session.execute(text(explain_sql))
            rows = result.fetchall()
            return "\n".join(str(row[0]) for row in rows)

    async def _execute(self, query: Query) -> QuerySet:
        stmt, extra_params = self._build_select_statement(query)
        async with self.session_maker() as session:
            result = await session.execute(stmt, extra_params or None)
            rows = [dict(row) for row in result.mappings()]
            return QuerySet(rows=rows, client=self, table=query.table, query=query)

    def _format_conditions(self, conditions: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
        if not conditions:
            return "", {}

        clauses: List[str] = []
        params: Dict[str, Any] = {}
        for index, (field, value) in enumerate(conditions.items()):
            param_name = f"cond_{index}"
            if isinstance(value, list):
                in_params = [f":{param_name}_{i}" for i in range(len(value))]
                clauses.append(f"`{field}` IN ({', '.join(in_params)})")
                for i, v in enumerate(value):
                    params[f"{param_name}_{i}"] = v
            else:
                clauses.append(f"`{field}` = :{param_name}")
                params[param_name] = value
        return " AND ".join(clauses), params

    async def _insert_one(
        self,
        table: str,
        data: Dict[str, Any],
        returning: Optional[List[str]] = None
    ) -> Optional[Dict[str, Any]]:
        columns = ", ".join(f"`{k}`" for k in data.keys())
        placeholders = ", ".join([f":{key}" for key in data.keys()])
        sql = f"INSERT INTO `{table}` ({columns}) VALUES ({placeholders})"

        # Serialize dict/list values to JSON strings for JSON columns.
        # pymysql (via aiomysql) cannot bind raw Python dict/list objects
        # as query parameters — unlike asyncpg which handles them natively.
        # Use ensure_ascii=False to send raw UTF-8 encoded Unicode characters
        # instead of \uXXXX escape sequences — Doris's JSON parser rejects
        # Unicode escapes for certain scripts (e.g., Persian/Arabic text).
        # Use compact separators for better parser compatibility.
        safe_data = {k: (json.dumps(v, ensure_ascii=False, separators=(',', ':')) if isinstance(v, (dict, list)) else v) for k, v in data.items()}

        async with self.session_maker() as session:
            await session.execute(text(sql), safe_data)
            await session.commit()
            return data

    async def _insert_many(
        self,
        table: str,
        data: List[Dict[str, Any]],
        returning: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        if not data:
            return []

        columns = ", ".join(f"`{k}`" for k in data[0].keys())
        placeholders = ", ".join([f":{key}" for key in data[0].keys()])
        sql = f"INSERT INTO `{table}` ({columns}) VALUES ({placeholders})"

        # Serialize dict/list values in each row (see _insert_one above).
        safe_data = [
            {k: (json.dumps(v, ensure_ascii=False, separators=(',', ':')) if isinstance(v, (dict, list)) else v) for k, v in row.items()}
            for row in data
        ]

        async with self.session_maker() as session:
            await session.execute(text(sql), safe_data)
            await session.commit()
            return data

    async def _update(
        self,
        table: str,
        conditions: Dict[str, Any],
        values: Dict[str, Any],
        limit: Optional[int] = None
    ) -> int:
        if not values:
            return 0

        # Serialize dict/list values to JSON strings for JSON columns
        # (same rationale as _insert_one).
        safe_values = {k: (json.dumps(v, ensure_ascii=False, separators=(',', ':')) if isinstance(v, (dict, list)) else v) for k, v in values.items()}

        set_clause = ", ".join([f"`{key}` = :{key}" for key in safe_values.keys()])
        where_clause, params = self._format_conditions(conditions)
        sql = f"UPDATE `{table}` SET {set_clause}"

        if where_clause:
            sql += f" WHERE {where_clause}"
        if limit:
            sql += f" LIMIT {limit}"

        async with self.session_maker() as session:
            result = await session.execute(text(sql), {**params, **safe_values})
            await session.commit()
            return result.rowcount

    async def _delete(
        self,
        table: str,
        conditions: Dict[str, Any],
        limit: Optional[int] = None
    ) -> int:
        where_clause, params = self._format_conditions(conditions)
        sql = f"DELETE FROM `{table}`"

        if where_clause:
            sql += f" WHERE {where_clause}"
        if limit:
            sql += f" LIMIT {limit}"

        async with self.session_maker() as session:
            result = await session.execute(text(sql), params)
            await session.commit()
            return result.rowcount

    def _build_select_statement(self, query: Query, for_explain: bool = False) -> Tuple[Select, Dict]:
        # Build SELECT with expression support — pass through raw SQL
        # expressions (e.g. "JSON_EXTRACT(...) AS channel_id") as text().
        #
        # Note: table aliases (e.g. "telegram_crawl_metrics rt") are NOT
        # used here — repository queries reference tables by their full names.
        # See TelegramCrawlMetricRepository for the convention.
        select_cols = [text(col) for col in query.selects]
        stmt: Select = select(*select_cols).select_from(text(f"`{query.table}`"))

        # -- JOINs ----------------------------------------------------------
        for join in query.joins:
            join_kw = {}
            if join.join_type.upper() == "LEFT":
                join_kw["isouter"] = True
            elif join.join_type.upper() == "FULL":
                join_kw["full"] = True
            stmt = stmt.join(
                text(join.table),
                text(join.on_clause),
                **join_kw,
            )

        # -- WHERE ----------------------------------------------------------
        where_clause = self._build_where_clause(query.wheres)
        if where_clause is not None:
            stmt = stmt.where(where_clause)

        # -- GROUP BY -------------------------------------------------------
        for col in query.group_by:
            stmt = stmt.group_by(text(col))

        # -- ORDER BY -------------------------------------------------------
        for col in query.order_by:
            if col.startswith("-"):
                stmt = stmt.order_by(text(f"{col[1:]} DESC"))
            else:
                stmt = stmt.order_by(text(col))

        # -- LIMIT / OFFSET -------------------------------------------------
        if query.limit is not None:
            stmt = stmt.limit(query.limit)
        if query.offset is not None:
            stmt = stmt.offset(query.offset)

        # Accumulated raw expression params (from RawExpr in where clauses)
        params = dict(query._params) if hasattr(query, '_params') else {}
        return stmt, params

    def _build_where_clause(self, conditions: List[WhereCondition]):
        if not conditions:
            return None

        and_clauses = []
        or_clauses = []

        for cond in conditions:
            if cond.group is not None:
                group_clause = self._build_group_clause(cond.group)
                if group_clause is not None:
                    if cond.is_or:
                        or_clauses.append(group_clause)
                    else:
                        and_clauses.append(group_clause)
            else:
                clause = self._build_single_condition(cond)
                if clause is not None:
                    if cond.is_or:
                        or_clauses.append(clause)
                    else:
                        and_clauses.append(clause)

        result_clauses = []
        if and_clauses:
            result_clauses.append(and_(*and_clauses) if len(and_clauses) > 1 else and_clauses[0])
        if or_clauses:
            result_clauses.append(or_(*or_clauses) if len(or_clauses) > 1 else or_clauses[0])

        if not result_clauses:
            return None
        if len(result_clauses) == 1:
            return result_clauses[0]
        return or_(*result_clauses)

    def _build_group_clause(self, group: WhereGroup):
        if not group.conditions:
            return None

        and_clauses = []
        or_clauses = []
        for cond in group.conditions:
            if cond.group is not None:
                clause = self._build_group_clause(cond.group)
            else:
                clause = self._build_single_condition(cond)

            if clause is not None:
                if cond.is_or:
                    or_clauses.append(clause)
                else:
                    and_clauses.append(clause)

        result_clauses = []
        if and_clauses:
            result_clauses.append(and_(*and_clauses) if len(and_clauses) > 1 else and_clauses[0])
        if or_clauses:
            result_clauses.append(or_(*or_clauses) if len(or_clauses) > 1 else or_clauses[0])

        if not result_clauses:
            return None
        if len(result_clauses) == 1:
            return result_clauses[0]
        return or_(*result_clauses)

    def _build_single_condition(self, cond: WhereCondition):
        if cond.column is None:
            return None
        # Column names use [a-z0-9_.] — safe without backtick quoting.
        # Dotted names (telegram_messages.message_sent_at) are passed
        # through as-is; Doris resolves them correctly.
        col = literal_column(cond.column)
        op = cond.operator

        if isinstance(op, Eq):    return col == op.value
        elif isinstance(op, Gt):  return col > op.value
        elif isinstance(op, Gte): return col >= op.value
        elif isinstance(op, Lt):  return col < op.value
        elif isinstance(op, Lte): return col <= op.value
        elif isinstance(op, NotEq): return col != op.value
        elif isinstance(op, In):  return col.in_(op.values)
        elif isinstance(op, Like): return col.like(op.pattern)
        elif isinstance(op, ILike): return col.like(op.pattern)
        elif isinstance(op, IsNull):     return col.is_(None)
        elif isinstance(op, IsNotNull):  return col.isnot(None)
        elif isinstance(op, RawExpr):
            return text(f"{cond.column} {op.sql}")
        return col == getattr(op, 'value', op)

    async def _bulk_update(self, table: str, ids: List[Any], values: Dict[str, Any]) -> int:
        # Serialize dict/list values to JSON strings for JSON columns
        # (same rationale as _insert_one).
        safe_values = {k: (json.dumps(v, ensure_ascii=False, separators=(',', ':')) if isinstance(v, (dict, list)) else v) for k, v in values.items()}

        set_clause = ", ".join([f"`{k}` = :{k}" for k in safe_values.keys()])
        sql = f"UPDATE `{table}` SET {set_clause} WHERE id IN :ids"
        params = {**safe_values, "ids": tuple(ids)}

        async with self.session_maker() as session:
            result = await session.execute(text(sql), params)
            await session.commit()
            return result.rowcount

    async def _bulk_delete(self, table: str, ids: List[Any]) -> int:
        sql = f"DELETE FROM `{table}` WHERE id IN :ids"

        async with self.session_maker() as session:
            result = await session.execute(text(sql), {"ids": tuple(ids)})
            await session.commit()
            return result.rowcount


async def ensure_doris_database_exists(url: str) -> None:
    """Ensure the target Doris database exists, creating it if necessary.

    Parses the database name from *url*, connects to the Doris instance without
    a database name, and issues ``CREATE DATABASE IF NOT EXISTS`` if the target
    is missing.

    Args:
        url: A full ``mysql+aiomysql://user:pass@host:port/dbname`` URL.
    """
    parsed = urlparse(url)
    db_name = parsed.path.lstrip("/")
    if not db_name:
        logger.warning("No database name in URL %r; skipping ensure-database check.", url)
        return

    # Build a URL without a database name so we can connect and issue CREATE DATABASE
    base_url = urlunparse(parsed._replace(path=""))

    engine = create_async_engine(base_url, pool_pre_ping=True)
    try:
        async with engine.connect() as conn:
            await conn.execute(text(f"CREATE DATABASE IF NOT EXISTS `{db_name}`"))
            await conn.commit()
        logger.info("Ensured Doris database %r exists.", db_name)
    finally:
        await engine.dispose()
