from __future__ import annotations

import argparse
from pathlib import Path
from typing import Dict, List, Optional

import duckdb
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import HTMLResponse
from rdflib import Graph, URIRef
import uvicorn

from .renderer import build_report_payload, render_report_html
from .manifest import (
    _cf_value,
    _iter_pipelines,
    _iter_reports,
    load_manifest_graph,
    parse_pipeline,
    resolve_manifest_paths,
)


def _local_name(iri: str) -> str:
    for sep in ("#", "/"):
        if sep in iri:
            return iri.rsplit(sep, 1)[-1]
    return iri


def _load_registry_from_paths(paths: List[Path]) -> Dict[str, dict]:
    registry: Dict[str, dict] = {}
    for path in paths:
        g = load_manifest_graph(path)
        reports: List[tuple[str, str]] = []
        for report in _iter_reports(g):
            pipeline = _cf_value(g, report, "reportForPipeline")
            if not isinstance(pipeline, URIRef):
                continue
            reports.append((str(report), str(pipeline)))
        if reports:
            for report_uri, pipeline_uri in reports:
                pipeline_id = _local_name(pipeline_uri)
                duckdb_path, parquet_root = _storage_from_manifest(path, g, pipeline_uri)
                registry[pipeline_id] = {
                    "manifest": str(path),
                    "report_uri": report_uri,
                    "pipeline_uri": pipeline_uri,
                    "duckdb_path": duckdb_path,
                    "parquet_root": parquet_root,
                }
            continue
        for pipeline in _iter_pipelines(g):
            pipeline_uri = str(pipeline)
            pipeline_id = _local_name(pipeline_uri)
            duckdb_path, parquet_root = _storage_from_manifest(path, g, pipeline_uri)
            registry[pipeline_id] = {
                "manifest": str(path),
                "report_uri": None,
                "pipeline_uri": pipeline_uri,
                "duckdb_path": duckdb_path,
                "parquet_root": parquet_root,
            }
    return registry


def _storage_from_manifest(path: Path, g: Graph, pipeline_uri: str) -> tuple[Optional[str], Optional[str]]:
    try:
        pipeline = parse_pipeline(g, pipeline_uri)
    except Exception:
        return None, None

    base_dir = None
    pipeline_id = None
    for node in pipeline.nodes:
        if not node.definition_uri.endswith("ParquetArchiveSinkStep"):
            continue
        params = node.parameters or {}
        pipeline_id = params.get("pipelineId") or params.get("pipeline_id")
        base_dir = params.get("baseDir") or params.get("base_dir")
        if pipeline_id and base_dir:
            break

    if not (pipeline_id and base_dir):
        return None, None

    base_path = Path(str(base_dir))
    if not base_path.is_absolute():
        base_path = (path.resolve().parent / base_path).resolve()
    pipeline_id_str = str(pipeline_id)
    parquet_root = str(base_path / pipeline_id_str)
    duckdb_path = str(Path(parquet_root) / (pipeline_id_str + ".duckdb"))
    return duckdb_path, parquet_root


def create_app(
    manifest_or_dir: str,
    *,
    duckdb_path: Optional[str] = None,
    duckdb_dir: Optional[str] = None,
) -> FastAPI:
    app = FastAPI(title="Cogniflow Pipeline Report")
    manifest_root = Path(manifest_or_dir).expanduser().resolve()
    manifest_paths = resolve_manifest_paths(manifest_root)
    registry: Dict[str, dict] = _load_registry_from_paths(manifest_paths)

    if not registry:
        raise ValueError("No cf:Report or cf:ProcessingPipeline entries found in provided manifests.")
    db_path = str(Path(duckdb_path).expanduser().resolve()) if duckdb_path else None
    db_dir = str(Path(duckdb_dir).expanduser().resolve()) if duckdb_dir else None

    @app.get("/", response_class=HTMLResponse)
    def report_index() -> str:
        rows = []
        for pipeline_id in sorted(registry.keys()):
            entry = registry.get(pipeline_id, {})
            rows.append(
                "<tr>"
                f"<td><a href=\"/report/{pipeline_id}\">{pipeline_id}</a></td>"
                f"<td>{entry.get('manifest', '')}</td>"
                "</tr>"
            )
        rows_html = "\n".join(rows) if rows else "<tr><td colspan='2'>None</td></tr>"
        return (
            "<!doctype html><html><head><meta charset='utf-8' />"
            "<title>Cogniflow Reports</title>"
            "<style>"
            "body{font-family:'IBM Plex Sans','Segoe UI',sans-serif;margin:24px;background:#0e1016;color:#e7e9ef;}"
            "table{width:100%;border-collapse:collapse;font-size:14px;background:#171a22;border:1px solid #242837;}"
            "th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #252c3b;}"
            "th{color:#4bc5b8;font-weight:600;}"
            "a{color:#4bc5b8;text-decoration:none;}"
            "</style></head><body>"
            "<h1>Pipeline Registry</h1>"
            "<table>"
            "<thead><tr><th>Pipeline</th><th>Manifest</th></tr></thead>"
            "<tbody>" + rows_html + "</tbody>"
            "</table>"
            "</body></html>"
        )

    @app.get("/pipelines")
    def list_pipelines() -> dict:
        payload = []
        for pipeline_id in sorted(registry.keys()):
            entry = registry.get(pipeline_id, {})
            item = {
                "pipeline_id": pipeline_id,
                "manifest": entry.get("manifest"),
                "pipeline_uri": entry.get("pipeline_uri"),
                "duckdb_path": entry.get("duckdb_path"),
            }
            payload.append(item)
        return {"pipelines": payload}

    @app.get("/report/{pipeline_id}", response_class=HTMLResponse)
    def report_view(pipeline_id: str) -> str:
        if pipeline_id not in registry:
            raise HTTPException(status_code=404, detail="Unknown pipeline id.")
        duckdb_url = None
        entry = registry.get(pipeline_id, {})
        if db_path or db_dir or entry.get("duckdb_path"):
            duckdb_url = f"/duckdb/{pipeline_id}/query"
        return render_report_html(
            data_url=f"/report/{pipeline_id}/data",
            duckdb_query_url=duckdb_url,
            pipeline_id=pipeline_id,
        )

    @app.get("/report/{pipeline_id}/data")
    def report_data(pipeline_id: str) -> dict:
        entry = registry.get(pipeline_id)
        if not entry:
            raise HTTPException(status_code=404, detail="Unknown pipeline id.")
        report_uri = entry.get("report_uri") or None
        return build_report_payload(entry["manifest"], report_uri=report_uri)

    @app.get("/duckdb/{pipeline_id}/query")
    def duckdb_query(
        pipeline_id: str,
        sql: str = Query(..., description="SQL SELECT query to run against the DuckDB file."),
    ) -> dict:
        if pipeline_id not in registry:
            raise HTTPException(status_code=404, detail="Unknown pipeline id.")
        entry = registry.get(pipeline_id, {})
        parquet_root = entry.get("parquet_root")
        if parquet_root and not (db_path or db_dir):
            root_path = Path(parquet_root)
            if not root_path.exists():
                raise HTTPException(status_code=400, detail="Parquet data directory not found.")
            has_parquet = any(root_path.rglob("*.parquet"))
            if not has_parquet:
                return {"columns": [], "rows": []}
            if not sql.strip().lower().startswith("select"):
                raise HTTPException(status_code=400, detail="Only SELECT queries are allowed.")
            try:
                con = duckdb.connect(":memory:")
                try:
                    glob = (root_path / "**" / "*.parquet").as_posix().replace("'", "''")
                    con.execute(
                        "CREATE OR REPLACE VIEW pipeline_data AS "
                        f"SELECT * FROM read_parquet('{glob}', hive_partitioning=true)"
                    )
                    df = con.execute(sql).fetchdf()
                finally:
                    con.close()
            except Exception as exc:
                raise HTTPException(status_code=400, detail=str(exc)) from exc
            return {
                "columns": list(df.columns),
                "rows": df.to_dict(orient="records"),
            }
        path = db_path
        if not path and db_dir:
            candidate = Path(db_dir) / (pipeline_id + ".duckdb")
            if candidate.exists():
                path = str(candidate)
            else:
                nested = Path(db_dir) / pipeline_id / (pipeline_id + ".duckdb")
                if nested.exists():
                    path = str(nested)
        if not path:
            candidate = entry.get("duckdb_path")
            if candidate and Path(candidate).exists():
                path = str(candidate)
        if not path:
            raise HTTPException(status_code=400, detail="DuckDB path not configured.")
        if not sql.strip().lower().startswith("select"):
            raise HTTPException(status_code=400, detail="Only SELECT queries are allowed.")
        try:
            con = duckdb.connect(path, read_only=True)
            try:
                df = con.execute(sql).fetchdf()
            finally:
                con.close()
        except Exception as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc
        return {
            "columns": list(df.columns),
            "rows": df.to_dict(orient="records"),
        }

    return app


def _build_parser(*, prog: str = "cf pipeline report") -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog=prog,
        description="Serve live Cogniflow reports backed by DuckDB.",
    )
    parser.add_argument("manifest_or_dir", help="Path to a manifest file or a directory of manifests.")
    parser.add_argument("--duckdb", help="Path to a DuckDB database file (read-only, shared by all pipelines).")
    parser.add_argument("--duckdb-dir", help="Directory containing per-pipeline DuckDB files (<pipeline_id>.duckdb).")
    parser.add_argument("--host", default="127.0.0.1", help="Host to bind (default: 127.0.0.1).")
    parser.add_argument("--port", type=int, default=8765, help="Port to bind (default: 8765).")
    return parser


def run(
    argv: list[str] | None = None,
    *,
    prog: str = "cf pipeline report",
) -> int:
    parser = _build_parser(prog=prog)
    args = parser.parse_args(argv)

    app = create_app(
        args.manifest_or_dir,
        duckdb_path=args.duckdb,
        duckdb_dir=args.duckdb_dir,
    )
    uvicorn.run(app, host=args.host, port=args.port)
    return 0


def main() -> int:
    return run()


if __name__ == "__main__":
    raise SystemExit(main())
