"""
BioPype REST API Application.

FastAPI backend that exposes BioPype core functionality via REST endpoints.
Designed to be used with a React frontend (or any HTTP client).

API design: load YAML → normalized dict → edit in UI → generate YAML → save to file
"""

from typing import List

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware

from biopype_builder.core import SnippetLoader

from .models import ErrorResponse, SnippetMetadata

# Create FastAPI app
app = FastAPI(
    title="BioPype Builder API",
    description="REST API for building BioPype pipelines",
    version="2.0.0",
    docs_url="/api/docs",
    redoc_url="/api/redoc",
    openapi_url="/api/openapi.json",
)

# CORS middleware for React frontend
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",  # Vite dev server
        "http://localhost:3000",  # Alternative dev port
        "http://localhost:8000",  # Builder integrated UI
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Initialize loader (singleton pattern)
_loader = None


def get_loader() -> SnippetLoader:
    """Get or create the SnippetLoader singleton."""
    global _loader
    if _loader is None:
        _loader = SnippetLoader()
        _loader.discover_snippets()
        _loader.discover_pipelines()
    return _loader


# Health check
@app.get("/api/health")
async def health_check():
    """Health check endpoint."""
    return {"status": "ok", "service": "biopype-api"}


# Snippet/Pipeline discovery
@app.get(
    "/api/snippets",
    response_model=List[SnippetMetadata],
    summary="List all snippets",
)
async def list_snippets():
    """Get all available snippets (excluding pipelines)."""
    loader = get_loader()
    return loader.snippets_metadata


@app.get(
    "/api/pipelines",
    response_model=List[SnippetMetadata],
    summary="List all pipelines",
)
async def list_pipelines():
    """Get all available pipelines (excluding snippets)."""
    loader = get_loader()
    return loader.pipelines_metadata


@app.get(
    "/api/snippets/{snippet_name}",
    response_model=SnippetMetadata,
    summary="Get snippet metadata",
    responses={404: {"model": ErrorResponse}},
)
async def get_snippet(snippet_name: str):
    """Get metadata for a specific snippet."""
    loader = get_loader()
    snippet = loader.get_snippet(snippet_name)

    if snippet is None:
        raise HTTPException(
            status_code=404, detail=f"Snippet '{snippet_name}' not found"
        )

    return snippet


@app.get(
    "/api/pipelines/{pipeline_name}",
    response_model=SnippetMetadata,
    summary="Get pipeline metadata",
    responses={404: {"model": ErrorResponse}},
)
async def get_pipeline_endpoint(pipeline_name: str):
    """Get metadata for a specific pipeline."""
    loader = get_loader()
    pipeline = loader.get_pipeline(pipeline_name)

    if pipeline is None:
        raise HTTPException(
            status_code=404, detail=f"Pipeline '{pipeline_name}' not found"
        )

    return pipeline


# Refresh snippets (useful for development)
@app.post("/api/snippets/refresh", summary="Refresh snippet and pipeline cache")
async def refresh_snippets():
    """Refresh the snippet and pipeline cache."""
    global _loader
    _loader = SnippetLoader()
    _loader.discover_snippets()
    _loader.discover_pipelines()
    return {
        "message": "Snippets and pipelines refreshed",
        "snippets": len(_loader.snippets_metadata),
        "pipelines": len(_loader.pipelines_metadata),
    }


@app.get("/api/pipeline-files", summary="List available pipeline files")
async def list_pipeline_files():
    """List all available pipeline YAML files in PYPE_PIPELINES directory."""
    from pathlib import Path

    from pype.__config__ import PYPE_PIPELINES
    from pype.misc import package_files

    try:
        pipelines = package_files(PYPE_PIPELINES, ".yaml")
        pipeline_files = []

        for pipeline_path in pipelines:
            p = Path(pipeline_path)
            pipeline_files.append(
                {
                    "name": p.stem,
                    "path": str(p),
                    "directory": str(p.parent),
                }
            )

        # Sort by name
        pipeline_files.sort(key=lambda x: x["name"])

        return {
            "files": pipeline_files,
            "pype_modules": PYPE_PIPELINES.__path__[0]
            if hasattr(PYPE_PIPELINES, "__path__")
            else str(PYPE_PIPELINES),
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/api/load-yaml", response_model=dict)
async def load_yaml_file(request: dict):
    """
    Load and parse pipeline YAML with minimal transformation.

    Only normalization: alias keys → canonical keys (e.g., -i → --input)
    Batch arg names (no dash prefix) are preserved as-is.
    Returns YAML dict structure ready for direct editing in UI.
    """
    from pathlib import Path

    import yaml

    from biopype_builder.core.key_normalization import normalize_step_arguments

    try:
        file_path = request.get("file_path")
        if not file_path:
            raise HTTPException(status_code=400, detail="file_path is required")

        yaml_path = Path(file_path).expanduser()
        if not yaml_path.exists():
            raise HTTPException(status_code=404, detail=f"File not found: {file_path}")

        # Parse YAML
        with open(yaml_path, "r") as f:
            pipeline_data = yaml.safe_load(f)

        # Get loader for metadata fetching
        loader = get_loader()

        def snippet_fetcher(name: str):
            """Fetch snippet or pipeline metadata by name."""
            metadata = loader.get_snippet(name)
            if metadata is None:
                metadata = loader.get_pipeline(name)
            return metadata

        # Normalize all step argument keys (alias → canonical)
        steps = pipeline_data.get("steps", {})
        normalized_steps = {}

        for step_id, step_data in steps.items():
            normalized_step = normalize_step_arguments(step_data, snippet_fetcher)
            normalized_steps[step_id] = normalized_step

        # Extract pipeline info
        info = pipeline_data.get("info", {})
        pipeline_name = yaml_path.stem

        return {
            "success": True,
            "pipeline_name": pipeline_name,
            "description": info.get("description", ""),
            "variable_descriptions": info.get("arguments", {}),
            "defaults": info.get("defaults", {}),
            "choices": info.get("choices", {}),
            "batches": info.get("batches", {}),
            "template_paths": info.get("template_paths", {}),
            "template_arguments": info.get("template_arguments", {}),
            "steps": normalized_steps,
        }

    except HTTPException:
        raise
    except Exception as e:
        import traceback

        traceback.print_exc()
        raise HTTPException(status_code=400, detail=str(e))


@app.post("/api/generate-yaml", response_model=dict)
async def generate_yaml(request: dict):
    """
    Generate YAML string from pipeline dict.

    Takes the normalized pipeline dict and converts directly to YAML string.
    No denormalization needed - canonical forms are valid in YAML.
    """
    import yaml

    try:
        pipeline_dict = request.get("pipeline")
        if not pipeline_dict:
            raise HTTPException(status_code=400, detail="pipeline dict is required")

        # Generate YAML directly - no transformation needed
        yaml_str = yaml.dump(
            pipeline_dict,
            default_flow_style=False,
            sort_keys=False,
            allow_unicode=True,
        )

        return {"yaml": yaml_str}

    except HTTPException:
        raise
    except Exception as e:
        import traceback

        traceback.print_exc()
        raise HTTPException(status_code=400, detail=str(e))


@app.post("/api/run-pipeline-test", summary="Run pype.commands to show pipeline info")
async def run_pipeline_test(request: dict):
    """
    Spawn pype.commands in a subprocess and return its output.

    Modes:
      "info"  — pype.commands pipelines <name>  (show argparse help / pipeline info)
      "run"   — pype.commands run <name>         (future: dry-run queue)
    """
    import os
    import re
    import subprocess
    import sys

    pipeline_name = request.get("pipeline_name", "").strip()
    mode = request.get("mode", "info")

    if not pipeline_name:
        raise HTTPException(status_code=400, detail="pipeline_name is required")

    if mode == "info":
        cmd = [sys.executable, "-m", "pype.commands", "pipelines", pipeline_name]
    elif mode == "run":
        cmd = [sys.executable, "-m", "pype.commands", "run", pipeline_name]
    else:
        raise HTTPException(status_code=400, detail=f"Unknown mode: {mode}")

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            env=os.environ.copy(),
            timeout=30,
        )
        ansi = re.compile(r"\x1b\[[0-9;]*[mK]")
        return {
            "stdout": ansi.sub("", result.stdout),
            "stderr": ansi.sub("", result.stderr),
            "returncode": result.returncode,
            "command": " ".join(cmd[2:]),
        }
    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=408, detail="Command timed out after 30s")
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


def _parse_run_help(text: str) -> list:
    """
    Parse the combined stdout+stderr of `pype run <name>` (no args → error+help).

    Returns a list of dicts:
      { name, section, is_bool, choices, description, default, required }
    """
    import re

    # Required names from the argparse error line
    required: set[str] = set()
    m = re.search(r"the following arguments are required: (.+?)(?:\n|$)", text)
    if m:
        required = {n.strip().lstrip("-") for n in m.group(1).split(",")}

    # section header: word(s) followed by ':'  at start of line (not indented)
    section_re = re.compile(r"^([A-Z][\w ]+):$")
    # arg line: exactly 2 spaces + '--name' + optional metavar + optional inline desc
    arg_re = re.compile(r"^  (--[\w-]+)(?:\s+(\{[^}]+\}|[A-Z][A-Z_0-9]*))?(?:\s+(.*))?$")
    # description continuation: 24-space indent
    desc_re = re.compile(r"^ {24}(.+)$")

    args: list[dict] = []
    current_section: str | None = None
    current_arg: dict | None = None
    desc_lines: list[str] = []

    def flush() -> None:
        if current_arg is None:
            return
        desc = " ".join(desc_lines).strip()
        default_m = re.search(r"Default:\s*([^.]+?)(?:\.|$)", desc)
        current_arg["description"] = desc
        current_arg["default"] = default_m.group(1).strip() if default_m else None
        current_arg["required"] = current_arg["name"] in required
        args.append(current_arg)

    skip_sections = {"usage", "error", "options", "Options"}

    for line in text.splitlines():
        sm = section_re.match(line)
        if sm:
            flush()
            current_arg = None
            desc_lines = []
            s = sm.group(1)
            current_section = None if s in skip_sections else s
            continue

        if current_section is None:
            continue

        am = arg_re.match(line)
        if am:
            flush()
            desc_lines = []
            name = am.group(1).lstrip("-")
            metavar = am.group(2)       # None → boolean flag
            inline = am.group(3) or ""
            choices = None
            if metavar and metavar.startswith("{"):
                choices = [c.strip() for c in metavar[1:-1].split(",")]
                metavar = None
            current_arg = {
                "name": name,
                "section": current_section,
                "is_bool": metavar is None and choices is None,
                "choices": choices,
                "metavar": metavar,
            }
            if inline:
                desc_lines.append(inline)
            continue

        dm = desc_re.match(line)
        if dm and current_arg is not None:
            desc_lines.append(dm.group(1))

    flush()

    # Deduplicate (argparse error+help is printed twice in combined output)
    seen: set[str] = set()
    unique: list[dict] = []
    for a in args:
        if a["name"] not in seen:
            seen.add(a["name"])
            unique.append(a)
    return unique


@app.get("/api/pipeline-args/{pipeline_name}", summary="Get argparse args for a pipeline interface")
async def get_pipeline_args(pipeline_name: str, interface: str = "run"):
    """
    Call `pype.commands <interface> <name>` with no pipeline args to trigger
    the argparse error+help, then parse the output into a structured arg list.

    interface: "run" or "pipelines"
    """
    import os
    import re
    import subprocess
    import sys

    if interface not in ("run", "pipelines"):
        raise HTTPException(status_code=400, detail="interface must be 'run' or 'pipelines'")

    cmd = [sys.executable, "-m", "pype.commands", interface, pipeline_name]
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            env=os.environ.copy(),
            timeout=30,
        )
        combined = result.stdout + "\n" + result.stderr
        ansi = re.compile(r"\x1b\[[0-9;]*[mK]")
        combined = ansi.sub("", combined)
        args = _parse_run_help(combined)
        if not args:
            raise HTTPException(status_code=404, detail=f"No args found for pipeline '{pipeline_name}' via '{interface}' — is it saved to PYPE_MODULES?")
        return {"pipeline_name": pipeline_name, "interface": interface, "args": args}
    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=408, detail="pype --help timed out")
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/api/dry-run", summary="Run pipeline dry run and return runtime DAG")
async def dry_run_pipeline(request: dict):
    """
    Run pype.commands run --queue dry_run --log <tempdir> <pipeline_name> [--var val ...].

    Uses a fresh temporary directory as the log root so the pipeline_runtime.yaml
    location is deterministic and isolated from other runs.
    Returns the parsed YAML along with stdout/stderr for diagnostics.
    """
    import os
    import re
    import subprocess
    import sys
    import tempfile
    from pathlib import Path

    import yaml

    pipeline_name = request.get("pipeline_name", "").strip()
    args = request.get("args", {})  # {var_name: value}

    if not pipeline_name:
        raise HTTPException(status_code=400, detail="pipeline_name is required")

    # Fresh temp dir — only one run will ever write here
    log_root = tempfile.mkdtemp(prefix="biopype_dryrun_")

    interface = request.get("interface", "run")  # "run" or "pipelines"
    if interface not in ("run", "pipelines"):
        raise HTTPException(status_code=400, detail="interface must be 'run' or 'pipelines'")

    cmd = [
        sys.executable, "-m", "pype.commands",
        interface, "--queue", "dry_run", "--log", log_root,
        pipeline_name,
    ]
    for var_name, value in args.items():
        if value is None or not str(value).strip():
            continue
        if str(value).strip() == "true":
            cmd.append(f"--{var_name}")  # boolean flag
        else:
            cmd.extend([f"--{var_name}", str(value)])

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            env=os.environ.copy(),
            timeout=120,
        )
        ansi = re.compile(r"\x1b\[[0-9;]*[mK]")
        stdout = ansi.sub("", result.stdout)
        stderr = ansi.sub("", result.stderr)

        # dry_run.post_run() prints log.__path__ to stdout.
        # pipeline_runtime.yaml lives one level up: os.path.dirname(log.__path__).
        # We search all stdout lines for one that is a subdirectory of log_root,
        # which is robust against any trailing prints after post_run().
        runtime_data = None
        log_dir = None
        for raw_line in stdout.splitlines():
            line = raw_line.strip()
            if not line:
                continue
            p = Path(line)
            if not p.is_absolute():
                p = Path.cwd() / p
            # The printed path is log.__path__ — a child of log_root
            try:
                p.relative_to(log_root)
            except ValueError:
                continue
            candidate = p / "pipeline_runtime.yaml"
            if candidate.exists():
                log_dir = str(p)
                with open(candidate) as f:
                    runtime_data = yaml.safe_load(f)
                break

        return {
            "runtime": runtime_data,
            "log_dir": log_dir,
            "stdout": stdout,
            "stderr": stderr,
            "returncode": result.returncode,
            "command": " ".join(cmd[2:]),
        }
    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=408, detail="Dry run timed out after 120s")
    except Exception as e:
        import traceback

        traceback.print_exc()
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/api/save-yaml", summary="Save pipeline YAML to file")
async def save_yaml_file(request: dict):
    """
    Save YAML string to a file in PYPE_PIPELINES directory.

    Args:
        request: Dict with 'filename', 'yaml', and optional 'overwrite' keys
    """
    from pathlib import Path

    from pype.__config__ import PYPE_PIPELINES

    try:
        filename = request.get("filename")
        yaml_content = request.get("yaml")
        overwrite = request.get("overwrite", False)

        if not filename:
            raise HTTPException(status_code=400, detail="filename is required")
        if not yaml_content:
            raise HTTPException(status_code=400, detail="yaml content is required")

        # Ensure .yaml extension
        if not filename.endswith(".yaml"):
            filename = f"{filename}.yaml"

        # Get pipelines directory
        if hasattr(PYPE_PIPELINES, "__path__"):
            pipelines_dir = Path(PYPE_PIPELINES.__path__[0])
        else:
            pipelines_dir = Path(str(PYPE_PIPELINES))

        # Construct full path
        file_path = pipelines_dir / filename

        # Check if file exists
        if file_path.exists() and not overwrite:
            raise HTTPException(
                status_code=409,
                detail=f"File {filename} already exists. Confirm to overwrite.",
            )

        # Write file
        with open(file_path, "w") as f:
            f.write(yaml_content)

        return {
            "success": True,
            "message": f"Pipeline saved to {file_path}",
            "path": str(file_path),
            "overwritten": overwrite and file_path.exists(),
        }
    except HTTPException:
        raise
    except Exception as e:
        import traceback

        traceback.print_exc()
        raise HTTPException(status_code=500, detail=str(e))
