"""Unified API endpoints for mdash — PostgreSQL, BigQuery, Redis, and S3 (multi-instance)."""

from __future__ import annotations

from importlib.metadata import version as pkg_version

from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel

from .db_adapter import BigQueryAdapter, DbAdapter, PostgresAdapter
from .redis_client import create_redis_client, get_key_info, scan_keys
from .s3_client import create_s3_client, get_object_info, list_buckets, list_objects
from .sql_builder import build_aggregation_query, build_query, parse_filter_entries

router = APIRouter(prefix="/api")

MAX_SCAN_KEYS = 50_000


# ─── Helpers ─────────────────────────────────────────────────────────

def _get_db_adapter(request: Request, instance_id: str) -> DbAdapter:
    adapter = request.app.state.db_adapters.get(instance_id)
    if not adapter:
        raise HTTPException(status_code=404, detail=f"Unknown database instance: {instance_id}")
    return adapter


def _get_redis_client(request: Request, instance_id: str):
    client = request.app.state.redis_clients.get(instance_id)
    if not client:
        raise HTTPException(status_code=404, detail=f"Unknown Redis instance: {instance_id}")
    return client


def _get_s3_client(request: Request, instance_id: str):
    client = request.app.state.s3_clients.get(instance_id)
    if not client:
        raise HTTPException(status_code=404, detail=f"Unknown S3 instance: {instance_id}")
    return client


def _make_instance_id(label: str) -> str:
    """Sanitize a label into a URL-safe instance ID."""
    return label.strip().lower().replace(" ", "-").replace("/", "-")


def _unique_id(existing: dict, base: str) -> str:
    """Return a unique ID based on base, appending -2, -3, etc. if needed."""
    iid = _make_instance_id(base)
    if iid not in existing:
        return iid
    counter = 2
    while f"{iid}-{counter}" in existing:
        counter += 1
    return f"{iid}-{counter}"


# ─── Health ──────────────────────────────────────────────────────────

@router.get("/health")
async def health(request: Request):
    page_size = getattr(request.app.state, "page_size", 100)
    sources = {"postgres": [], "redis": [], "s3": []}

    for iid, adapter in request.app.state.db_adapters.items():
        try:
            ok = await adapter.health_check()
            status = "connected" if ok else "error"
        except Exception:
            status = "error"
        sources.setdefault(adapter.db_type, []).append(
            {"id": iid, "label": iid, "status": status}
        )

    for iid, client in request.app.state.redis_clients.items():
        try:
            await client.ping()
            sources["redis"].append({"id": iid, "label": iid, "status": "connected"})
        except Exception:
            sources["redis"].append({"id": iid, "label": iid, "status": "error"})

    for iid, client in request.app.state.s3_clients.items():
        try:
            await client.list_buckets()
            sources["s3"].append({"id": iid, "label": iid, "status": "connected"})
        except Exception:
            sources["s3"].append({"id": iid, "label": iid, "status": "error"})

    for label in getattr(request.app.state, "s3_failed", set()):
        sources["s3"].append({"id": label, "label": label, "status": "error"})

    try:
        ver = pkg_version("mdash")
    except Exception:
        ver = "dev"

    return {"status": "ok", "sources": sources, "page_size": page_size, "version": ver}


# ─── SQL Databases (PostgreSQL, BigQuery, etc.) ─────────────────────

class PgConnectRequest(BaseModel):
    connection_string: str
    label: str = ""


@router.post("/pg/connect")
async def pg_connect(body: PgConnectRequest, request: Request):
    from urllib.parse import urlparse
    label = body.label.strip()
    if not label:
        try:
            parsed = urlparse(body.connection_string)
            label = parsed.path.lstrip("/") or parsed.hostname or "postgres"
        except Exception:
            label = "postgres"
    iid = _unique_id(request.app.state.db_adapters, label)
    try:
        adapter = PostgresAdapter(body.connection_string)
        await adapter.connect()
        request.app.state.db_adapters[iid] = adapter
        return {"status": "connected", "instance_id": iid, "label": label}
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))


class BigQueryConnectRequest(BaseModel):
    project: str
    dataset: str
    credentials_path: str = ""
    label: str = ""


@router.post("/bigquery/connect")
async def bigquery_connect(body: BigQueryConnectRequest, request: Request):
    label = body.label.strip() or f"{body.project}/{body.dataset}"
    iid = _unique_id(request.app.state.db_adapters, label)
    try:
        adapter = BigQueryAdapter(
            project=body.project,
            dataset=body.dataset,
            credentials_path=body.credentials_path or None,
        )
        await adapter.connect()
        request.app.state.db_adapters[iid] = adapter
        return {"status": "connected", "instance_id": iid, "label": label}
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))


@router.get("/pg/{instance_id}/tables")
async def pg_list_tables(instance_id: str, request: Request):
    adapter = _get_db_adapter(request, instance_id)
    tables = await adapter.list_tables()
    return {"tables": tables}


@router.get("/pg/{instance_id}/tables/{schema}/{table}/columns")
async def pg_list_columns(instance_id: str, schema: str, table: str, request: Request):
    adapter = _get_db_adapter(request, instance_id)
    columns = await adapter.list_columns(schema, table)
    if not columns:
        raise HTTPException(status_code=404, detail=f"Table {schema}.{table} not found")
    return {"columns": columns}


class PgQueryRequest(BaseModel):
    schema_: str = "public"
    table: str
    include: str = ""
    exclude: str = ""
    highlight: str = ""
    columns: str = ""
    limit: int = 500
    offset: int = 0
    # Aggregation fields (optional — when set, switches to aggregation mode)
    view_mode: str | None = None
    aggregation: str | None = None
    agg_column: str | None = None
    group_by_column: str | None = None
    value_column: str | None = None

    class Config:
        populate_by_name = True

    def __init__(self, **data):
        if "schema" in data:
            data["schema_"] = data.pop("schema")
        super().__init__(**data)


@router.post("/pg/{instance_id}/query")
async def pg_query(instance_id: str, body: PgQueryRequest, request: Request):
    adapter = _get_db_adapter(request, instance_id)
    dialect = adapter.dialect

    valid_columns = await adapter.get_column_names(body.schema_, body.table)
    if not valid_columns:
        raise HTTPException(status_code=404, detail=f"Table {body.schema_}.{body.table} not found")

    # Aggregation mode (number or chart view)
    if body.view_mode in ("number", "chart"):
        try:
            agg_col = body.agg_column or body.value_column
            agg_sql, params = build_aggregation_query(
                schema=body.schema_,
                table=body.table,
                valid_columns=valid_columns,
                aggregation=body.aggregation or "count",
                column=agg_col,
                group_by_column=body.group_by_column,
                include=body.include,
                exclude=body.exclude,
                dialect=dialect,
            )
        except ValueError as e:
            raise HTTPException(status_code=400, detail=str(e))

        rows = await adapter.fetch(agg_sql, params)
        display_sql = f"{agg_sql} -- params: {params}"

        if body.view_mode == "number":
            val = rows[0]["value"] if rows else 0
            agg_name = (body.aggregation or "count").upper()
            label = agg_name if agg_name == "COUNT" else f"{agg_name} of {agg_col}"
            return {
                "view_mode": "number",
                "value": round(float(val), 2) if val is not None else 0,
                "label": label,
                "sql": display_sql,
            }
        else:
            results = [
                {
                    "group": str(r["group"]) if r["group"] is not None else "(null)",
                    "value": round(float(r["value"]), 2) if r["value"] is not None else 0,
                }
                for r in rows
            ]
            return {
                "view_mode": "chart",
                "results": results,
                "sql": display_sql,
            }

    # Regular table mode
    try:
        data_sql, count_sql, params = build_query(
            schema=body.schema_,
            table=body.table,
            valid_columns=valid_columns,
            include=body.include,
            exclude=body.exclude,
            columns=body.columns,
            limit=body.limit,
            offset=body.offset,
            dialect=dialect,
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))

    rows = await adapter.fetch(data_sql, params)
    count_val = await adapter.fetchval(count_sql, params)

    highlight_filters = parse_filter_entries(body.highlight)
    display_sql = f"{data_sql} -- params: {params}"

    return {
        "rows": rows,
        "total_count": count_val,
        "sql": display_sql,
        "highlight_filters": highlight_filters,
    }


# ─── Redis ───────────────────────────────────────────────────────────

class RedisConnectRequest(BaseModel):
    connection_string: str
    label: str = ""


@router.post("/redis/connect")
async def redis_connect(body: RedisConnectRequest, request: Request):
    from urllib.parse import urlparse
    label = body.label.strip()
    if not label:
        try:
            parsed = urlparse(body.connection_string)
            host = parsed.hostname or "redis"
            port = parsed.port or 6379
            label = f"{host}:{port}"
        except Exception:
            label = "redis"
    iid = _unique_id(request.app.state.redis_clients, label)
    try:
        client = await create_redis_client(body.connection_string)
        await client.ping()
        request.app.state.redis_clients[iid] = client
        request.app.state.redis_key_caches[iid] = {}
        return {"status": "connected", "instance_id": iid, "label": label}
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))


def _normalize_pattern(raw: str) -> str:
    pattern = raw.strip() if raw.strip() else "*"
    if not any(c in pattern for c in ("*", "?", "[", "]")):
        pattern = f"*{pattern}*"
    return pattern


async def _scan_all_keys(client, pattern: str) -> list[str]:
    collected: set[str] = set()
    cur = 0
    while True:
        next_cur, keys = await scan_keys(client, pattern=pattern, count=500, cursor=cur)
        collected.update(keys)
        cur = next_cur
        if cur == 0 or len(collected) >= MAX_SCAN_KEYS:
            break
    return sorted(collected)


class RedisKeysRequest(BaseModel):
    pattern: str = "*"
    offset: int = 0
    limit: int = 100


@router.post("/redis/{instance_id}/keys")
async def redis_list_keys(instance_id: str, body: RedisKeysRequest, request: Request):
    client = _get_redis_client(request, instance_id)

    pattern = _normalize_pattern(body.pattern)
    cache: dict = request.app.state.redis_key_caches.get(instance_id, {})

    try:
        if cache.get("pattern") != pattern:
            all_keys = await _scan_all_keys(client, pattern)
            cache["pattern"] = pattern
            cache["keys"] = all_keys
            request.app.state.redis_key_caches[instance_id] = cache

        all_keys = cache["keys"]
        total = len(all_keys)
        page_keys = all_keys[body.offset : body.offset + body.limit]

        if page_keys:
            pipe = client.pipeline(transaction=False)
            for k in page_keys:
                pipe.type(k)
            types = await pipe.execute()
            rows = [{"key": k, "type": t} for k, t in zip(page_keys, types)]
        else:
            rows = []

        return {
            "rows": rows,
            "total_count": total,
            "has_more": body.offset + body.limit < total,
            "offset": body.offset,
            "pattern": pattern,
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/redis/{instance_id}/keys/{key:path}")
async def redis_get_key(instance_id: str, key: str, request: Request):
    client = _get_redis_client(request, instance_id)
    try:
        info = await get_key_info(client, key)
        if info["type"] == "none":
            raise HTTPException(status_code=404, detail=f"Key not found: {key}")
        return info
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


# ─── S3 ──────────────────────────────────────────────────────────────

class S3ConnectRequest(BaseModel):
    endpoint_url: str = ""
    access_key: str = ""
    secret_key: str = ""
    region: str = "us-east-1"
    label: str = ""


@router.post("/s3/connect")
async def s3_connect(body: S3ConnectRequest, request: Request):
    label = body.label.strip()
    if not label:
        if body.endpoint_url:
            from urllib.parse import urlparse
            try:
                parsed = urlparse(body.endpoint_url)
                label = parsed.hostname or "s3"
            except Exception:
                label = "s3"
        else:
            label = "s3"
    # Check both active and failed S3 connections for unique ID
    s3_failed = getattr(request.app.state, "s3_failed", set())
    combined = {**request.app.state.s3_clients, **{k: None for k in s3_failed}}
    iid = _unique_id(combined, label)
    try:
        ctx = create_s3_client(
            endpoint_url=body.endpoint_url or None,
            access_key=body.access_key or None,
            secret_key=body.secret_key or None,
            region=body.region,
        )
        client = await ctx.__aenter__()
        request.app.state._s3_ctxs[iid] = ctx
        request.app.state.s3_clients[iid] = client
        s3_failed.discard(iid)
        await client.list_buckets()
        return {"status": "connected", "instance_id": iid, "label": label}
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))


@router.get("/s3/{instance_id}/buckets")
async def s3_get_buckets(instance_id: str, request: Request):
    client = _get_s3_client(request, instance_id)
    try:
        buckets = await list_buckets(client)
        return {"buckets": buckets}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


class S3KeysRequest(BaseModel):
    bucket: str
    prefix: str = ""
    continuation_token: str | None = None
    limit: int = 100


@router.post("/s3/{instance_id}/keys")
async def s3_list_keys(instance_id: str, body: S3KeysRequest, request: Request):
    client = _get_s3_client(request, instance_id)
    try:
        result = await list_objects(
            client,
            bucket=body.bucket,
            prefix=body.prefix,
            continuation_token=body.continuation_token,
            max_keys=body.limit,
        )
        rows = result["objects"]
        return {
            "rows": rows,
            "total_count": len(rows),
            "has_more": result["is_truncated"],
            "next_token": result["next_token"],
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/s3/{instance_id}/keys/{bucket}/{key:path}")
async def s3_get_key(instance_id: str, bucket: str, key: str, request: Request):
    client = _get_s3_client(request, instance_id)
    try:
        info = await get_object_info(client, bucket, key)
        return info
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
