from __future__ import annotations

import logging
import math
from pathlib import Path
from typing import Any

import pandas as pd
from hdbcli import dbapi  # SAP HANA Client

from hanaflow.ingestion.schema_builder import (
    ColumnDef,
    build_column_defs,
    create_schema_sql,
    create_table_sql,
    derive_names,
)

logger = logging.getLogger(__name__)
_BATCH_SIZE = 1_000



class UploadResult:
    """Holds the outcome of a successful upload."""

    def __init__(
        self,
        schema_name: str,
        table_name: str,
        rows_inserted: int,
        columns: list[ColumnDef],
        schema_created: bool,
        table_replaced: bool,
    ) -> None:
        self.schema_name    = schema_name
        self.table_name     = table_name
        self.rows_inserted  = rows_inserted
        self.columns        = columns
        self.schema_created = schema_created
        self.table_replaced = table_replaced

    def __repr__(self) -> str:  # pragma: no cover
        return (
            f"UploadResult(schema={self.schema_name!r}, "
            f"table={self.table_name!r}, rows={self.rows_inserted})"
        )


# ---------------------------------------------------------------------------
# Core upload function
# ---------------------------------------------------------------------------

def upload(
    connection: dbapi.Connection,
    df: pd.DataFrame,
    file_path: str | Path,
    schema_name: str | None = None,
    table_name:  str | None = None,
    replace: bool = False,
    batch_size: int = _BATCH_SIZE,
) -> UploadResult:
    """
    Upload *df* to SAP HANA.

    Parameters
    ----------
    connection   : open hdbcli.dbapi.Connection
    df           : DataFrame (columns already sanitised by reader.py)
    file_path    : original file path – used to derive schema/table names
    schema_name  : override auto-derived schema name (optional)
    table_name   : override auto-derived table name (optional)
    replace      : if True, DROP TABLE before CREATE TABLE
    batch_size   : rows per executemany() call

    Returns
    -------
    UploadResult
    """
    # --- 1. Derive names ---------------------------------------------------
    auto_schema, auto_table = derive_names(file_path)
    schema_name = (schema_name or auto_schema).upper()
    table_name  = (table_name  or auto_table ).upper()

    # --- 2. Build column definitions ---------------------------------------
    col_defs = build_column_defs(df)
    logger.debug("Column definitions: %s", col_defs)

    cur = connection.cursor()
    try:
        # --- 3. CREATE SCHEMA (ignore if already exists) ------------------
        schema_created = _ensure_schema(cur, schema_name)

        # --- 4. CREATE TABLE (optionally drop first) ----------------------
        table_replaced = _ensure_table(
            cur, schema_name, table_name, col_defs, replace=replace
        )
        connection.commit()

        # --- 5. Batch INSERT ----------------------------------------------
        rows_inserted = _insert_dataframe(
            cur, connection, df, schema_name, table_name, col_defs, batch_size
        )

    finally:
        cur.close()

    return UploadResult(
        schema_name    = schema_name,
        table_name     = table_name,
        rows_inserted  = rows_inserted,
        columns        = col_defs,
        schema_created = schema_created,
        table_replaced = table_replaced,
    )


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _schema_exists(cur: dbapi.Cursor, schema_name: str) -> bool:
    cur.execute(
        "SELECT COUNT(*) FROM SYS.SCHEMAS WHERE SCHEMA_NAME = ?",
        (schema_name,),
    )
    row = cur.fetchone()
    return bool(row and row[0] > 0)


def _table_exists(cur: dbapi.Cursor, schema_name: str, table_name: str) -> bool:
    cur.execute(
        "SELECT COUNT(*) FROM SYS.TABLES "
        "WHERE SCHEMA_NAME = ? AND TABLE_NAME = ?",
        (schema_name, table_name),
    )
    row = cur.fetchone()
    return bool(row and row[0] > 0)


def _ensure_schema(cur: dbapi.Cursor, schema_name: str) -> bool:
    """Create the schema if it does not already exist. Returns True if created."""
    if _schema_exists(cur, schema_name):
        logger.info("Schema %r already exists — skipping CREATE SCHEMA.", schema_name)
        return False

    sql = create_schema_sql(schema_name)
    logger.info("Creating schema: %s", sql)
    cur.execute(sql)
    return True


def _ensure_table(
    cur: dbapi.Cursor,
    schema_name: str,
    table_name: str,
    col_defs: list[ColumnDef],
    replace: bool,
) -> bool:
    """Create the table. If replace=True, drop it first. Returns True if dropped."""
    replaced = False

    if _table_exists(cur, schema_name, table_name):
        if replace:
            drop_sql = f'DROP TABLE "{schema_name}"."{table_name}"'
            logger.info("Dropping existing table: %s", drop_sql)
            cur.execute(drop_sql)
            replaced = True
        else:
            raise RuntimeError(
                f'Table "{schema_name}"."{table_name}" already exists. '
                "Pass --replace to overwrite it."
            )

    sql = create_table_sql(schema_name, table_name, col_defs)
    logger.info("Creating table:\n%s", sql)
    cur.execute(sql)
    return replaced


def _prepare_row(row: tuple[Any, ...], col_defs: list[ColumnDef]) -> tuple[Any, ...]:
    import math
    import numpy as np  # numpy is a pandas dependency, always present

    result: list[Any] = []
    for val, col_def in zip(row, col_defs):
        # pandas NA / NaT → None (SQL NULL)
        if val is pd.NaT or val is pd.NA:
            result.append(None)
            continue

        # numpy integers → Python int
        if isinstance(val, (np.integer,)):
            result.append(int(val))
            continue

        # numpy floats → Python float; NaN → None
        if isinstance(val, (np.floating, float)):
            result.append(None if math.isnan(val) else float(val))
            continue

        # numpy bool → Python bool
        if isinstance(val, np.bool_):
            result.append(bool(val))
            continue

        # pandas Timestamp → Python datetime (hdbcli handles datetime natively)
        if isinstance(val, pd.Timestamp):
            result.append(val.to_pydatetime() if not pd.isna(val) else None)
            continue

        # timedelta → total seconds as int
        if isinstance(val, pd.Timedelta):
            result.append(int(val.total_seconds()))
            continue

        # Everything else (str, None, Python int/float/bool) passes through
        result.append(val)

    return tuple(result)


def _insert_dataframe(
    cur: dbapi.Cursor,
    connection: dbapi.Connection,
    df: pd.DataFrame,
    schema_name: str,
    table_name: str,
    col_defs: list[ColumnDef],
    batch_size: int,
) -> int:
    col_list    = ", ".join(f'"{c.name}"' for c in col_defs)
    placeholders = ", ".join("?" * len(col_defs))
    insert_sql  = (
        f'INSERT INTO "{schema_name}"."{table_name}" '
        f"({col_list}) VALUES ({placeholders})"
    )
    logger.debug("INSERT template: %s", insert_sql)

    total_rows  = len(df)
    total_batches = math.ceil(total_rows / batch_size)
    rows_inserted = 0

    logger.info(
        "Inserting %d rows in %d batch(es) of up to %d …",
        total_rows, total_batches, batch_size,
    )

    for batch_num, start in enumerate(range(0, total_rows, batch_size), start=1):
        chunk = df.iloc[start : start + batch_size]

        # Build list of tuples — hdbcli.executemany() expects list[tuple]
        rows: list[tuple[Any, ...]] = [
            _prepare_row(tuple(row), col_defs)
            for row in chunk.itertuples(index=False, name=None)
        ]

        cur.executemany(insert_sql, rows)
        connection.commit()          # commit after every batch

        rows_inserted += len(rows)
        pct = rows_inserted / total_rows * 100
        logger.info(
            "Batch %d/%d committed — %d/%d rows (%.1f%%)",
            batch_num, total_batches, rows_inserted, total_rows, pct,
        )

    return rows_inserted