"""Utilities for working with Tableau Hyper extracts."""

from __future__ import annotations

import csv
from datetime import datetime
from pathlib import Path
from typing import Any, Iterable, List, Tuple, TYPE_CHECKING

if TYPE_CHECKING:  # pragma: no cover
    from g_gremlin.tableau.template_models import TemplateManifest


def _load_hyperapi():
    try:
        from tableauhyperapi import (
            HyperProcess,
            Connection,
            Telemetry,
            CreateMode,
            TableDefinition,
            TableName,
            SqlType,
            Inserter,
        )
    except ImportError as exc:  # pragma: no cover - optional dependency
        raise ImportError(
            "tableauhyperapi is required for Hyper commands. "
            "Install with: pip install g-gremlin[tableau-hyper]"
        ) from exc

    return {
        "HyperProcess": HyperProcess,
        "Connection": Connection,
        "Telemetry": Telemetry,
        "CreateMode": CreateMode,
        "TableDefinition": TableDefinition,
        "TableName": TableName,
        "SqlType": SqlType,
        "Inserter": Inserter,
    }


def _load_pandas():
    try:
        import pandas as pd
    except ImportError as exc:  # pragma: no cover - optional dependency
        raise ImportError(
            "pandas is required for template binding. Install with: pip install g-gremlin[tableau]"
        ) from exc
    return pd


def _is_int(value: str) -> bool:
    try:
        int(value)
        return True
    except Exception:
        return False


def _is_float(value: str) -> bool:
    try:
        float(value)
        return True
    except Exception:
        return False


def _parse_datetime(value: str) -> datetime | None:
    try:
        return datetime.fromisoformat(value)
    except Exception:
        return None


def _infer_column_type(values: Iterable[str]) -> str:
    has_value = False
    all_int = True
    all_float = True
    all_date = True
    any_time = False

    for raw in values:
        value = (raw or "").strip()
        if value == "":
            continue
        has_value = True
        if not _is_int(value):
            all_int = False
        if not _is_float(value):
            all_float = False
        dt = _parse_datetime(value)
        if dt is None:
            all_date = False
        else:
            if dt.time() != datetime.min.time():
                any_time = True

    if not has_value:
        return "text"
    if all_int:
        return "int"
    if all_float:
        return "float"
    if all_date:
        return "timestamp" if any_time else "date"
    return "text"


def _convert_value(raw: str, kind: str) -> Any:
    value = (raw or "").strip()
    if value == "":
        return None
    if kind == "int":
        return int(value)
    if kind == "float":
        return float(value)
    if kind == "date":
        dt = _parse_datetime(value)
        if dt:
            return dt.date()
        return None
    if kind == "timestamp":
        return _parse_datetime(value)
    return value


def _infer_schema(rows: List[List[str]], headers: List[str]) -> List[Tuple[str, str]]:
    columns: List[List[str]] = [[] for _ in headers]
    for row in rows:
        for idx, raw in enumerate(row):
            if idx < len(columns):
                columns[idx].append(raw)

    schema: List[Tuple[str, str]] = []
    for name, values in zip(headers, columns):
        schema.append((name, _infer_column_type(values)))
    return schema


def create_hyper_from_csv(
    csv_path: Path,
    hyper_path: Path,
    table_name: str = "Extract",
    sample_size: int = 500,
) -> Path:
    """Create a .hyper extract from CSV with auto-detected schema."""
    hyperapi = _load_hyperapi()

    with csv_path.open(newline="", encoding="utf-8") as handle:
        reader = csv.reader(handle)
        headers = next(reader, None)
        if not headers:
            raise ValueError("CSV file has no header row")
        sample_rows: List[List[str]] = []
        for idx, row in enumerate(reader):
            sample_rows.append(row)
            if idx + 1 >= sample_size:
                break

    schema = _infer_schema(sample_rows, headers)

    HyperProcess = hyperapi["HyperProcess"]
    Connection = hyperapi["Connection"]
    Telemetry = hyperapi["Telemetry"]
    CreateMode = hyperapi["CreateMode"]
    TableDefinition = hyperapi["TableDefinition"]
    TableName = hyperapi["TableName"]
    SqlType = hyperapi["SqlType"]
    Inserter = hyperapi["Inserter"]

    table_def = TableDefinition(
        TableName("Extract", table_name),
        columns=[
            TableDefinition.Column(name, _sql_type_from_kind(SqlType, kind))
            for name, kind in schema
        ],
    )

    with HyperProcess(Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU) as hyper:
        with Connection(
            endpoint=hyper.endpoint,
            database=hyper_path,
            create_mode=CreateMode.CREATE_AND_REPLACE,
        ) as connection:
            connection.catalog.create_schema("Extract")
            connection.catalog.create_table(table_def)

            with csv_path.open(newline="", encoding="utf-8") as handle:
                reader = csv.reader(handle)
                next(reader, None)  # header
                with Inserter(connection, table_def) as inserter:
                    for row in reader:
                        converted = [
                            _convert_value(value, schema[idx][1]) if idx < len(schema) else None
                            for idx, value in enumerate(row)
                        ]
                        inserter.add_row(converted)
                    inserter.execute()

    return hyper_path


def _sql_type_from_kind(SqlType, kind: str):
    if kind == "int":
        return SqlType.int()
    if kind == "float":
        return SqlType.double()
    if kind == "date":
        return SqlType.date()
    if kind == "timestamp":
        return SqlType.timestamp()
    return SqlType.text()


def inspect_hyper(hyper_path: Path) -> dict:
    """Inspect a .hyper file and return schema information."""
    hyperapi = _load_hyperapi()
    HyperProcess = hyperapi["HyperProcess"]
    Connection = hyperapi["Connection"]
    Telemetry = hyperapi["Telemetry"]

    with HyperProcess(Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU) as hyper:
        with Connection(endpoint=hyper.endpoint, database=hyper_path) as connection:
            tables = []
            try:
                table_names = connection.catalog.get_table_names("Extract")
            except Exception:
                table_names = connection.catalog.get_table_names()

            for table in table_names:
                table_def = connection.catalog.get_table_definition(table)
                columns = [
                    {"name": col.name.unescaped, "type": str(col.type)}
                    for col in table_def.columns
                ]
                tables.append({"name": str(table), "columns": columns})

    return {"tables": tables}


def export_hyper_to_csv(hyper_path: Path, csv_path: Path) -> Path:
    """Export a .hyper extract to CSV."""
    hyperapi = _load_hyperapi()
    HyperProcess = hyperapi["HyperProcess"]
    Connection = hyperapi["Connection"]
    Telemetry = hyperapi["Telemetry"]

    with HyperProcess(Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU) as hyper:
        with Connection(endpoint=hyper.endpoint, database=hyper_path) as connection:
            table_names = connection.catalog.get_table_names("Extract")
            if not table_names:
                raise ValueError("No tables found in hyper file")
            table = table_names[0]
            table_def = connection.catalog.get_table_definition(table)
            columns = [col.name.unescaped for col in table_def.columns]
            query = f'SELECT * FROM {table}'
            rows = connection.execute_list_query(query)

    with csv_path.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.writer(handle)
        writer.writerow(columns)
        for row in rows:
            writer.writerow(row)

    return csv_path


def create_canonical_hyper(
    csv_path: Path,
    output_path: Path,
    role_mapping: dict,
    template_manifest: "TemplateManifest",
    table_name: str = "Extract",
) -> Path:
    """
    Create a .hyper file with columns renamed to canonical role names.

    All canonical roles (required + optional) are guaranteed to exist.
    """
    pd = _load_pandas()
    hyperapi = _load_hyperapi()

    df = pd.read_csv(csv_path)
    rename_dict = {csv_col: canonical_role for canonical_role, csv_col in role_mapping.items()}
    df_canonical = df.rename(columns=rename_dict)

    all_role_ids = [role.id for role in template_manifest.canonical_roles]
    for role_id in all_role_ids:
        if role_id not in df_canonical.columns:
            df_canonical[role_id] = pd.NA

    df_final = df_canonical[all_role_ids]

    for role_spec in template_manifest.canonical_roles:
        if role_spec.id not in df_final.columns:
            continue
        if role_spec.id not in role_mapping:
            continue
        # Only coerce if desired_type is explicitly set
        if role_spec.desired_type and role_spec.desired_type.upper() == "DATE":
            try:
                df_final[role_spec.id] = pd.to_datetime(df_final[role_spec.id], errors="raise")
            except Exception as exc:
                raise ValueError(
                    f"Failed to parse '{role_spec.id}' as date: {exc}. "
                    "Ensure column has ISO8601 or valid date strings."
                ) from exc
        elif role_spec.desired_type and role_spec.desired_type.upper() in ("DOUBLE", "INTEGER", "NUMERIC"):
            try:
                df_final[role_spec.id] = pd.to_numeric(df_final[role_spec.id], errors="raise")
            except Exception as exc:
                raise ValueError(
                    f"Failed to parse '{role_spec.id}' as numeric: {exc}. Ensure column contains only numbers."
                ) from exc

    HyperProcess = hyperapi["HyperProcess"]
    Connection = hyperapi["Connection"]
    Telemetry = hyperapi["Telemetry"]
    CreateMode = hyperapi["CreateMode"]
    TableDefinition = hyperapi["TableDefinition"]
    TableName = hyperapi["TableName"]
    SqlType = hyperapi["SqlType"]
    Inserter = hyperapi["Inserter"]

    columns = []
    for role_spec in template_manifest.canonical_roles:
        sql_type = _sql_type_for_role(SqlType, role_spec)
        columns.append(TableDefinition.Column(role_spec.id, sql_type))

    table_def = TableDefinition(TableName("Extract", table_name), columns=columns)

    with HyperProcess(Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU) as hyper:
        with Connection(
            endpoint=hyper.endpoint,
            database=output_path,
            create_mode=CreateMode.CREATE_AND_REPLACE,
        ) as connection:
            connection.catalog.create_schema("Extract")
            connection.catalog.create_table(table_def)

            with Inserter(connection, table_def) as inserter:
                for row in df_final.itertuples(index=False, name=None):
                    inserter.add_row([_normalize_value(value) for value in row])
                inserter.execute()

    return output_path


def _sql_type_for_role(SqlType, role_spec) -> Any:
    """Determine SQL type based on explicit desired_type, otherwise infer from first semantic type."""
    desired = (role_spec.desired_type or "").upper()
    if desired in {"INT", "INTEGER", "BIGINT"}:
        return SqlType.big_int()
    if desired in {"DOUBLE", "FLOAT", "NUMERIC"}:
        return SqlType.double()
    if desired in {"DATE"}:
        return SqlType.date()
    if desired in {"TIMESTAMP", "DATETIME"}:
        return SqlType.timestamp()

    # No desired_type specified - use primary semantic type (first in list)
    if role_spec.semantic_types:
        primary_type = role_spec.semantic_types[0].lower()
        if primary_type in {"integer", "int"}:
            return SqlType.big_int()
        if primary_type in {"numeric", "float", "double"}:
            return SqlType.double()
        if primary_type == "date":
            return SqlType.date()

    # Default to text for string/category/unknown types
    return SqlType.text()


def _normalize_value(value: Any) -> Any:
    try:
        import pandas as pd  # type: ignore

        if value is pd.NA:
            return None
        if pd.isna(value):
            return None
    except Exception:
        pass
    return value
