"""
Deployment commands for Cacao apps.

Supports multiple deployment targets:
- Docker — Generate a Dockerfile for containerized deployment
- Hugging Face Spaces — Dockerfile + README.md for HF Spaces
- Railway — railway.toml + Dockerfile
- Render — render.yaml + Dockerfile
- Fly.io — fly.toml + Dockerfile
- GitHub Pages — Already handled by `cacao build`
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

from .commands import (
    BOLD,
    CYAN,
    DARK_BROWN,
    DIM,
    GREEN,
    RED,
    RESET,
    _get_logo,
    find_app_instance,
    load_app_module,
)

# ---------------------------------------------------------------------------
# Dockerfile generation
# ---------------------------------------------------------------------------

_DOCKERFILE_TEMPLATE = """\
# --- Cacao App ---
# Generated by: cacao docker {app_file}
FROM python:3.12-slim

WORKDIR /app

# Install system dependencies (none required for most apps)
RUN apt-get update && apt-get install -y --no-install-recommends \\
    && rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Expose port
EXPOSE {port}

# Run the app
CMD ["cacao", "run", "{app_file}", "--host", "0.0.0.0", "--port", "{port}", "--no-reload"]
"""

_REQUIREMENTS_HEADER = "# Generated by: cacao docker\ncacao\nuvicorn[standard]\n"


def _detect_extra_deps(app_path: Path) -> list[str]:
    """Scan the app file for common import patterns and suggest extra deps."""
    extras: list[str] = []
    try:
        source = app_path.read_text(encoding="utf-8")
    except Exception:
        return extras

    dep_map: dict[str, str] = {
        "pandas": "pandas",
        "polars": "polars",
        "numpy": "numpy",
        "plotly": "plotly",
        "matplotlib": "matplotlib",
        "PIL": "pillow",
        "sklearn": "scikit-learn",
        "openai": "openai",
        "anthropic": "anthropic",
        "prompture": "prompture",
        "tukuy": "tukuy",
        "httpx": "httpx",
        "requests": "requests",
    }

    for import_name, pip_name in dep_map.items():
        if f"import {import_name}" in source or f"from {import_name}" in source:
            extras.append(pip_name)

    return extras


def _generate_dockerfile(
    app_file: str,
    port: int = 7860,
) -> str:
    """Generate a Dockerfile for the app."""
    return _DOCKERFILE_TEMPLATE.format(app_file=app_file, port=port)


def _generate_requirements(app_path: Path) -> str:
    """Generate requirements.txt content."""
    lines = [_REQUIREMENTS_HEADER.rstrip()]

    extras = _detect_extra_deps(app_path)
    for dep in extras:
        lines.append(dep)

    # Check if a requirements.txt already exists with extra deps
    existing = app_path.parent / "requirements.txt"
    if existing.exists():
        try:
            for line in existing.read_text(encoding="utf-8").splitlines():
                stripped = line.strip()
                if stripped and not stripped.startswith("#"):
                    # Normalize: take the package name (before any version specifier)
                    pkg_name = stripped.split(">=")[0].split("==")[0].split("[")[0].strip().lower()
                    if pkg_name not in ("cacao", "uvicorn") and stripped not in lines:
                        lines.append(stripped)
        except Exception:
            pass

    return "\n".join(lines) + "\n"


def _generate_dockerignore() -> str:
    """Generate .dockerignore content."""
    return """\
__pycache__
*.pyc
.git
.gitignore
.env
.venv
venv
node_modules
dist/
*.egg-info
.mypy_cache
.pytest_cache
.ruff_cache
"""


# ---------------------------------------------------------------------------
# Hugging Face Spaces
# ---------------------------------------------------------------------------

_HF_README_TEMPLATE = """\
---
title: {title}
emoji: 🤎
colorFrom: brown
colorTo: orange
sdk: docker
pinned: false
license: mit
---

# {title}

Built with [Cacao](https://github.com/cacao-research/Cacao) — a reactive web framework for Python.
"""


def _generate_hf_space(
    app_path: Path,
    app_file: str,
    title: str,
    port: int = 7860,
) -> dict[str, str]:
    """Generate files for Hugging Face Spaces deployment.

    Returns:
        Dict mapping filename -> content.
    """
    files: dict[str, str] = {}

    # Dockerfile (HF Spaces uses port 7860 by default)
    files["Dockerfile"] = _generate_dockerfile(app_file, port=port)
    files[".dockerignore"] = _generate_dockerignore()
    files["requirements.txt"] = _generate_requirements(app_path)
    files["README.md"] = _HF_README_TEMPLATE.format(title=title)

    return files


# ---------------------------------------------------------------------------
# Railway
# ---------------------------------------------------------------------------

_RAILWAY_TOML = """\
[build]
builder = "dockerfile"
dockerfilePath = "Dockerfile"

[deploy]
startCommand = "cacao run {app_file} --host 0.0.0.0 --port ${{PORT:-{port}}} --no-reload"
healthcheckPath = "/"
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 3
"""


def _generate_railway(
    app_file: str,
    port: int = 7860,
) -> dict[str, str]:
    """Generate files for Railway deployment."""
    files: dict[str, str] = {}
    files["railway.toml"] = _RAILWAY_TOML.format(app_file=app_file, port=port)
    files["Dockerfile"] = _generate_dockerfile(app_file, port=port)
    files[".dockerignore"] = _generate_dockerignore()
    return files


# ---------------------------------------------------------------------------
# Render
# ---------------------------------------------------------------------------

_RENDER_YAML = """\
services:
  - type: web
    name: {name}
    runtime: docker
    dockerfilePath: ./Dockerfile
    envVars:
      - key: PORT
        value: "{port}"
    healthCheckPath: /
"""


def _generate_render(
    app_file: str,
    name: str,
    port: int = 7860,
) -> dict[str, str]:
    """Generate files for Render deployment."""
    files: dict[str, str] = {}
    files["render.yaml"] = _RENDER_YAML.format(name=name, port=port)
    files["Dockerfile"] = _generate_dockerfile(app_file, port=port)
    files[".dockerignore"] = _generate_dockerignore()
    return files


# ---------------------------------------------------------------------------
# Fly.io
# ---------------------------------------------------------------------------

_FLY_TOML = """\
app = "{name}"
primary_region = "iad"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = {port}
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0

[[vm]]
  cpu_kind = "shared"
  cpus = 1
  memory_mb = 256
"""


def _generate_flyio(
    app_file: str,
    name: str,
    port: int = 8080,
) -> dict[str, str]:
    """Generate files for Fly.io deployment."""
    files: dict[str, str] = {}
    files["fly.toml"] = _FLY_TOML.format(name=name, port=port)
    files["Dockerfile"] = _generate_dockerfile(app_file, port=port)
    files[".dockerignore"] = _generate_dockerignore()
    return files


# ---------------------------------------------------------------------------
# `cacao docker` command
# ---------------------------------------------------------------------------


def docker_command(args: list[str]) -> None:
    """
    Generate a Dockerfile for a Cacao app.

    Usage: cacao docker app.py [options]
    """
    parser = argparse.ArgumentParser(
        prog="cacao docker",
        description="Generate a Dockerfile for a Cacao application",
    )
    parser.add_argument("app_file", help="Path to the app file (e.g., app.py)")
    parser.add_argument(
        "--port",
        "-p",
        type=int,
        default=7860,
        help="Port to expose (default: 7860)",
    )
    parser.add_argument(
        "-o",
        "--output",
        default=".",
        help="Output directory (default: current directory)",
    )
    parser.add_argument(
        "--no-requirements",
        action="store_true",
        help="Don't generate requirements.txt",
    )

    parsed = parser.parse_args(args)
    app_path = Path(parsed.app_file)

    if not app_path.exists():
        print(f"{RED}Error: File '{parsed.app_file}' not found{RESET}")
        sys.exit(1)

    output_dir = Path(parsed.output)
    output_dir.mkdir(parents=True, exist_ok=True)

    print(f"{CYAN}Generating Docker files...{RESET}")

    # Generate Dockerfile
    dockerfile_content = _generate_dockerfile(parsed.app_file, port=parsed.port)
    dockerfile_path = output_dir / "Dockerfile"
    dockerfile_path.write_text(dockerfile_content, encoding="utf-8")
    print(f"  {GREEN}Created{RESET} Dockerfile")

    # Generate .dockerignore
    dockerignore_path = output_dir / ".dockerignore"
    if not dockerignore_path.exists():
        dockerignore_path.write_text(_generate_dockerignore(), encoding="utf-8")
        print(f"  {GREEN}Created{RESET} .dockerignore")
    else:
        print(f"  {DIM}Skipped{RESET} .dockerignore (already exists)")

    # Generate requirements.txt
    if not parsed.no_requirements:
        req_path = output_dir / "requirements.txt"
        if not req_path.exists():
            req_content = _generate_requirements(app_path)
            req_path.write_text(req_content, encoding="utf-8")
            print(f"  {GREEN}Created{RESET} requirements.txt")
        else:
            print(f"  {DIM}Skipped{RESET} requirements.txt (already exists)")

    print()
    print("To build and run:")
    print(f"  {CYAN}docker build -t my-cacao-app .{RESET}")
    print(f"  {CYAN}docker run -p {parsed.port}:{parsed.port} my-cacao-app{RESET}")
    print()


# ---------------------------------------------------------------------------
# `cacao deploy` command — guided setup
# ---------------------------------------------------------------------------

_DEPLOY_TARGETS = {
    "1": ("hf", "Hugging Face Spaces", "Free GPU/CPU, great for ML demos"),
    "2": ("docker", "Docker", "Generate Dockerfile for any container host"),
    "3": ("railway", "Railway", "One-click deploy from GitHub"),
    "4": ("render", "Render", "Free tier, auto-deploy from Git"),
    "5": ("flyio", "Fly.io", "Edge deployment, global distribution"),
    "6": ("ghpages", "GitHub Pages", "Static sites (already works via cacao build)"),
}


def _prompt_input(prompt: str, default: str = "") -> str:
    """Prompt user for input with optional default."""
    suffix = f" [{default}]" if default else ""
    try:
        value = input(f"  {prompt}{suffix}: ").strip()
    except (KeyboardInterrupt, EOFError):
        print("\nCancelled.")
        sys.exit(1)
    return value or default


def _get_app_title(app_path: Path) -> str:
    """Try to detect app title from the app file."""
    try:
        module = load_app_module(app_path)
        app_instance = find_app_instance(module)
        if hasattr(app_instance, "_title"):
            return str(app_instance._title)
    except Exception:
        pass
    return app_path.stem.replace("_", " ").replace("-", " ").title()


def _write_deploy_files(
    files: dict[str, str],
    output_dir: Path,
    force: bool = False,
) -> list[str]:
    """Write generated files to output directory. Returns list of written files."""
    written: list[str] = []
    for filename, content in files.items():
        filepath = output_dir / filename
        if filepath.exists() and not force:
            print(f"  {DIM}Skipped{RESET} {filename} (already exists)")
            continue
        filepath.write_text(content, encoding="utf-8")
        print(f"  {GREEN}Created{RESET} {filename}")
        written.append(filename)
    return written


def deploy_command(args: list[str]) -> None:
    """
    Deploy a Cacao app to the cloud with guided setup.

    Usage: cacao deploy app.py [target] [options]
    """
    parser = argparse.ArgumentParser(
        prog="cacao deploy",
        description="Deploy a Cacao app to the cloud",
    )
    parser.add_argument("app_file", help="Path to the app file (e.g., app.py)")
    parser.add_argument(
        "target",
        nargs="?",
        choices=["hf", "docker", "railway", "render", "flyio", "ghpages"],
        help="Deployment target (interactive if omitted)",
    )
    parser.add_argument(
        "--name",
        type=str,
        default=None,
        help="App/service name (auto-detected if omitted)",
    )
    parser.add_argument(
        "--port",
        "-p",
        type=int,
        default=None,
        help="Port to expose (target-specific default)",
    )
    parser.add_argument(
        "-o",
        "--output",
        default=".",
        help="Output directory (default: current directory)",
    )
    parser.add_argument(
        "--force",
        "-f",
        action="store_true",
        help="Overwrite existing files",
    )

    parsed = parser.parse_args(args)
    app_path = Path(parsed.app_file)

    if not app_path.exists():
        print(f"{RED}Error: File '{parsed.app_file}' not found{RESET}")
        sys.exit(1)

    if not app_path.suffix == ".py":
        print(f"{RED}Error: '{parsed.app_file}' is not a Python file{RESET}")
        sys.exit(1)

    # Print header
    print(_get_logo())
    print(f"  {DARK_BROWN}{'App':<12}{RESET}{app_path.resolve()}")
    print()

    # Select target
    target = parsed.target
    if not target:
        print(f"  {BOLD}Select deployment target:{RESET}")
        print()
        for key, (_, name, desc) in _DEPLOY_TARGETS.items():
            print(f"    {CYAN}{key}{RESET}  {name:<24} {DIM}{desc}{RESET}")
        print()

        choice = _prompt_input("Enter choice (1-6)")
        entry = _DEPLOY_TARGETS.get(choice)
        if not entry:
            print(f"{RED}Invalid choice: {choice}{RESET}")
            sys.exit(1)
        target = entry[0]

    # Resolve defaults
    app_file = parsed.app_file
    name = parsed.name or app_path.stem.replace("_", "-").lower()
    output_dir = Path(parsed.output)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Dispatch to target
    if target == "ghpages":
        _deploy_ghpages(app_file)
        return

    if target == "docker":
        port = parsed.port or 7860
        print(f"  {BOLD}Generating Docker files...{RESET}")
        print()
        files: dict[str, str] = {
            "Dockerfile": _generate_dockerfile(app_file, port=port),
            ".dockerignore": _generate_dockerignore(),
            "requirements.txt": _generate_requirements(app_path),
        }
        _write_deploy_files(files, output_dir, force=parsed.force)
        print()
        print(f"  {BOLD}Next steps:{RESET}")
        print(f"    {CYAN}docker build -t {name} .{RESET}")
        print(f"    {CYAN}docker run -p {port}:{port} {name}{RESET}")

    elif target == "hf":
        port = parsed.port or 7860
        title = _get_app_title(app_path)
        print(f"  {BOLD}Generating Hugging Face Spaces files...{RESET}")
        print()
        files = _generate_hf_space(app_path, app_file, title, port=port)
        _write_deploy_files(files, output_dir, force=parsed.force)
        print()
        print(f"  {BOLD}Next steps:{RESET}")
        print(f"    1. Create a new Space at {CYAN}https://huggingface.co/new-space{RESET}")
        print(f"       - Select {BOLD}Docker{RESET} as the SDK")
        print("    2. Push these files to your Space repo:")
        print(f"       {CYAN}git init && git add .{RESET}")
        print(
            f"       {CYAN}git remote add origin https://huggingface.co/spaces/YOUR_USER/{name}{RESET}"
        )
        print(f"       {CYAN}git push -u origin main{RESET}")

    elif target == "railway":
        port = parsed.port or 7860
        print(f"  {BOLD}Generating Railway files...{RESET}")
        print()
        files = _generate_railway(app_file, port=port)
        files["requirements.txt"] = _generate_requirements(app_path)
        _write_deploy_files(files, output_dir, force=parsed.force)
        print()
        print(f"  {BOLD}Next steps:{RESET}")
        print(f"    1. Install Railway CLI: {CYAN}npm i -g @railway/cli{RESET}")
        print(f"    2. Login: {CYAN}railway login{RESET}")
        print(f"    3. Deploy: {CYAN}railway up{RESET}")
        print(f"    {DIM}Or connect a GitHub repo at https://railway.app/new{RESET}")

    elif target == "render":
        port = parsed.port or 7860
        print(f"  {BOLD}Generating Render files...{RESET}")
        print()
        files = _generate_render(app_file, name, port=port)
        files["requirements.txt"] = _generate_requirements(app_path)
        _write_deploy_files(files, output_dir, force=parsed.force)
        print()
        print(f"  {BOLD}Next steps:{RESET}")
        print("    1. Push to GitHub")
        print(f"    2. Go to {CYAN}https://dashboard.render.com/new{RESET}")
        print(f"    3. Select {BOLD}Blueprint{RESET} and connect your repo")
        print(f"    {DIM}Render will auto-detect render.yaml and deploy{RESET}")

    elif target == "flyio":
        port = parsed.port or 8080
        print(f"  {BOLD}Generating Fly.io files...{RESET}")
        print()
        files = _generate_flyio(app_file, name, port=port)
        files["requirements.txt"] = _generate_requirements(app_path)
        _write_deploy_files(files, output_dir, force=parsed.force)
        print()
        print(f"  {BOLD}Next steps:{RESET}")
        print(f"    1. Install flyctl: {CYAN}curl -L https://fly.io/install.sh | sh{RESET}")
        print(f"    2. Login: {CYAN}fly auth login{RESET}")
        print(f"    3. Launch: {CYAN}fly launch --no-deploy{RESET}")
        print(f"    4. Deploy: {CYAN}fly deploy{RESET}")

    else:
        print(f"{RED}Unknown target: {target}{RESET}")
        sys.exit(1)

    print()


def _deploy_ghpages(app_file: str) -> None:
    """Handle GitHub Pages deployment (redirect to cacao build)."""
    print("  GitHub Pages uses static builds. Run:")
    print()
    print(f"    {CYAN}cacao build {app_file} --base-path /your-repo-name{RESET}")
    print()
    print(f"  Then deploy the {BOLD}dist/{RESET} folder to GitHub Pages.")
    print(f"  {DIM}See: https://pages.github.com/{RESET}")
    print()
