"""
FORGE CLI — Main entry point
All commands: generate, config, telegram, doctor, models, version
"""
from __future__ import annotations

import asyncio
import sys
import time
from pathlib import Path
from typing import Optional, Annotated

import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich import box

from forge import __version__
from forge.settings import Settings, CONFIG_FILE

app = typer.Typer(
    name="forge",
    help="FORGE AI — Production Mobile App Generator (Groq + NVIDIA NIM + Telegram)",
    add_completion=True,
    rich_markup_mode="rich",
    no_args_is_help=True,
    pretty_exceptions_show_locals=False,
)
console = Console()


def _load_settings_or_exit() -> Settings:
    settings = Settings.load()
    issues = settings.validate_keys()
    if issues:
        console.print("\n  [bold red]✗ Missing configuration:[/bold red]")
        for issue in issues:
            console.print(f"    [red]•[/red] {issue}")
        console.print("\n  Run [cyan]forge config[/cyan] to set up.\n")
        raise typer.Exit(1)
    return settings


# ─────────────────────────────────────────────────────────────────────────────
# forge generate
# ─────────────────────────────────────────────────────────────────────────────

@app.command("generate", help="Generate a complete production-ready React Native + Expo app.")
def generate(
    prompt: Annotated[Optional[str], typer.Argument(help='App description in plain English')] = None,
    output: Annotated[Path, typer.Option("--output", "-o", help="Output directory")] = Path("."),
    complexity: Annotated[str, typer.Option("--complexity", "-c", help="minimal|standard|full|enterprise")] = "standard",
    groq_key: Annotated[Optional[str], typer.Option("--groq-key", envvar="GROQ_API_KEY", help="Groq API key")] = None,
    nvidia_key: Annotated[Optional[str], typer.Option("--nvidia-key", envvar="NVIDIA_API_KEY", help="NVIDIA NIM API key")] = None,
    plan_model: Annotated[Optional[str], typer.Option("--plan-model", help="Groq model for planning")] = None,
    code_model: Annotated[Optional[str], typer.Option("--code-model", help="NVIDIA NIM model for codegen")] = None,
    typescript: Annotated[bool, typer.Option("--typescript/--javascript", help="Generate TypeScript or JavaScript")] = True,
    include_tests: Annotated[bool, typer.Option("--tests/--no-tests", help="Generate Jest test suite")] = True,
    include_ci: Annotated[bool, typer.Option("--ci/--no-ci", help="Generate GitHub Actions CI/CD")] = True,
    skip_validation: Annotated[bool, typer.Option("--skip-validation", help="Skip Agent 4 validation (faster)")] = False,
    dry_run: Annotated[bool, typer.Option("--dry-run", help="Plan only — no code generation")] = False,
    yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
    debug: Annotated[bool, typer.Option("--debug", hidden=True)] = False,
):
    from forge.utils.ui import (
        print_banner, print_divider, log_line, print_agent_header,
        render_plan_panel, render_success, set_start,
    )
    from forge.core.pipeline import Pipeline
    from forge.core.session import LogEntry, AgentRun, AgentStatus

    print_banner()

    # Load & override settings
    settings = Settings.load()
    if groq_key:
        settings.groq_api_key = groq_key
    if nvidia_key:
        settings.nvidia_api_key = nvidia_key
    if plan_model:
        settings.plan_model = plan_model
    if code_model:
        settings.code_model = code_model

    settings.complexity = complexity
    settings.output_dir = str(output.resolve())
    settings.typescript = typescript
    settings.include_tests = include_tests
    settings.include_ci = include_ci
    settings.skip_validation = skip_validation

    issues = settings.validate_keys()
    if issues:
        for issue in issues:
            console.print(f"  [red]✗[/red] {issue}")
        console.print("\n  Run [cyan]forge config[/cyan] or set env vars.\n")
        raise typer.Exit(1)

    # Get prompt
    if not prompt:
        console.print()
        try:
            prompt = console.input("  [bold yellow]Describe your app:[/bold yellow] ").strip()
        except (KeyboardInterrupt, EOFError):
            console.print("\n  Cancelled.\n")
            raise typer.Exit(0)

    if not prompt or len(prompt.strip()) < 5:
        console.print("  [red]✗[/red] Prompt too short.\n")
        raise typer.Exit(1)

    if complexity not in ("minimal", "standard", "full", "enterprise"):
        console.print(f"  [red]✗[/red] Invalid complexity. Use: minimal | standard | full | enterprise\n")
        raise typer.Exit(1)

    # Show config
    print_divider("CONFIGURATION")
    console.print(f"  Prompt:       [bold]{prompt}[/bold]")
    console.print(f"  Complexity:   {complexity}")
    console.print(f"  Plan model:   [red]{settings.plan_model}[/red] [dim](Groq)[/dim]")
    console.print(f"  Code model:   [green]{settings.code_model}[/green] [dim](NVIDIA NIM)[/dim]")
    console.print(f"  TypeScript:   {typescript}")
    console.print(f"  Tests:        {include_tests}")
    console.print(f"  CI/CD:        {include_ci}")
    console.print(f"  Output:       {settings.output_dir}")

    if not yes and not dry_run:
        console.print()
        try:
            ans = console.input("  [bold]Start generation? [Y/n]:[/bold] ").strip().lower()
            if ans and ans not in ("y", "yes", ""):
                console.print("  Aborted.\n")
                raise typer.Exit(0)
        except (KeyboardInterrupt, EOFError):
            console.print("\n  Aborted.\n")
            raise typer.Exit(0)

    set_start()
    pipeline = Pipeline(settings)
    current_agent = [None]

    def on_log(entry: LogEntry) -> None:
        log_line(entry)

    def on_agent_update(run: AgentRun) -> None:
        if run.status == AgentStatus.RUNNING:
            print_agent_header(run.name, run.engine)
        elif run.status == AgentStatus.DONE:
            elapsed_s = run.elapsed
            stats_str = " | ".join(f"{k}={v}" for k, v in run.stats.items())
            console.print(
                f"  [dim]  Done in {elapsed_s:.1f}s"
                + (f" — {stats_str}" if stats_str else "")
                + "[/dim]"
            )
        current_agent[0] = run.name

    def on_progress(current: int, total: int, path: str) -> None:
        if total > 0:
            pct = int(current / total * 100)
            bar_filled = int(pct / 5)
            bar = "█" * bar_filled + "░" * (20 - bar_filled)
            console.print(
                f"  [dim]  [{bar}] {pct:3d}%  {path}[/dim]",
                end="\r",
            )

    try:
        session = asyncio.run(
            pipeline.run(
                prompt=prompt,
                on_log=on_log,
                on_agent_update=on_agent_update,
                on_progress=on_progress,
            )
        )

        if dry_run:
            if session.plan:
                print_divider("DRY RUN — PLAN PREVIEW")
                render_plan_panel(session.plan)
            console.print("\n  [dim]Dry run complete. No code generated.[/dim]\n")
            raise typer.Exit(0)

        if session.needs_clarification and session.plan:
            print_divider("CLARIFICATION NEEDED")
            for i, q in enumerate(session.plan.clarifying_questions, 1):
                console.print(f"  [yellow]{i}.[/yellow] {q}")
            console.print(
                "\n  Re-run with a more detailed prompt, or answer these questions "
                "in your next prompt.\n"
            )
            raise typer.Exit(0)

        if session.plan:
            print_divider("PLAN SUMMARY")
            render_plan_panel(session.plan)

        print_divider("RESULT")
        render_success(session)

    except KeyboardInterrupt:
        console.print("\n\n  [yellow]Generation cancelled by user.[/yellow]\n")
        raise typer.Exit(130)
    except Exception as e:
        console.print(f"\n  [bold red]✗ ERROR:[/bold red] {e}\n")
        if debug:
            import traceback
            traceback.print_exc()
        else:
            console.print("  Re-run with [dim]--debug[/dim] for full traceback.\n")
        raise typer.Exit(1)


# ─────────────────────────────────────────────────────────────────────────────
# forge config
# ─────────────────────────────────────────────────────────────────────────────

@app.command("config", help="Configure API keys, models, and defaults interactively.")
def config_cmd(
    show: Annotated[bool, typer.Option("--show", help="Show current config")] = False,
    reset: Annotated[bool, typer.Option("--reset", help="Reset to defaults")] = False,
    set_groq: Annotated[Optional[str], typer.Option("--groq-key", help="Set Groq API key")] = None,
    set_nvidia: Annotated[Optional[str], typer.Option("--nvidia-key", help="Set NVIDIA key")] = None,
    set_tg_token: Annotated[Optional[str], typer.Option("--tg-token", help="Set Telegram bot token")] = None,
    set_tg_chat: Annotated[Optional[str], typer.Option("--tg-chat-id", help="Set Telegram chat ID")] = None,
):
    settings = Settings.load()

    if reset:
        from rich.prompt import Confirm
        if Confirm.ask("  Reset all config?", default=False):
            if CONFIG_FILE.exists():
                CONFIG_FILE.unlink()
            console.print("  [green]✓[/green] Config reset.\n")
        return

    if show:
        t = Table(box=box.SIMPLE, show_header=False)
        t.add_column("Setting", style="dim", width=22)
        t.add_column("Value")
        t.add_row("Groq API Key", settings.mask(settings.groq_api_key))
        t.add_row("NVIDIA API Key", settings.mask(settings.nvidia_api_key))
        t.add_row("Telegram Token", settings.mask(settings.telegram_bot_token))
        t.add_row("Telegram Chat ID", settings.telegram_chat_id or "[dim]not set[/dim]")
        t.add_row("Plan Model", settings.plan_model)
        t.add_row("Code Model", settings.code_model)
        t.add_row("Complexity", settings.complexity)
        t.add_row("TypeScript", str(settings.typescript))
        t.add_row("Tests", str(settings.include_tests))
        t.add_row("CI/CD", str(settings.include_ci))
        t.add_row("Output Dir", settings.output_dir)
        t.add_row("Config File", str(CONFIG_FILE))
        console.print("\n", t, "\n")
        return

    # CLI flag updates (non-interactive)
    if any([set_groq, set_nvidia, set_tg_token, set_tg_chat]):
        if set_groq:
            settings.groq_api_key = set_groq
        if set_nvidia:
            settings.nvidia_api_key = set_nvidia
        if set_tg_token:
            settings.telegram_bot_token = set_tg_token
        if set_tg_chat:
            settings.telegram_chat_id = set_tg_chat
        settings.save()
        console.print("  [green]✓[/green] Settings updated.\n")
        return

    # Full interactive onboarding
    from forge.utils.onboarding import run_onboarding
    run_onboarding(Settings)


# ─────────────────────────────────────────────────────────────────────────────
# forge telegram
# ─────────────────────────────────────────────────────────────────────────────

@app.command("telegram", help="Start the FORGE Telegram bot (interactive mode).")
def telegram_cmd(
    token: Annotated[Optional[str], typer.Option("--token", envvar="TELEGRAM_BOT_TOKEN")] = None,
    chat_id: Annotated[Optional[str], typer.Option("--chat-id", envvar="TELEGRAM_CHAT_ID")] = None,
):
    settings = Settings.load()
    if token:
        settings.telegram_bot_token = token
    if chat_id:
        settings.telegram_chat_id = chat_id

    tg_issues = settings.validate_telegram()
    gen_issues = settings.validate_keys()

    if tg_issues:
        console.print("\n  [red]✗ Telegram not configured:[/red]")
        for i in tg_issues:
            console.print(f"    • {i}")
        console.print("\n  Run [cyan]forge config[/cyan] to set up Telegram.\n")
        raise typer.Exit(1)

    if gen_issues:
        console.print("\n  [yellow]⚠ Generation API keys missing:[/yellow]")
        for i in gen_issues:
            console.print(f"    • {i}")
        console.print("  The bot will start but generation will fail until keys are set.\n")

    try:
        from forge.telegram.bot import ForgeBot, TELEGRAM_AVAILABLE
        if not TELEGRAM_AVAILABLE:
            console.print("  [red]✗[/red] python-telegram-bot not installed.")
            console.print("  Run: [cyan]pip install 'forge-ai[telegram]'[/cyan]\n")
            raise typer.Exit(1)
    except ImportError:
        console.print("  [red]✗[/red] Telegram dependencies missing.")
        console.print("  Run: [cyan]pip install 'forge-ai[telegram]'[/cyan]\n")
        raise typer.Exit(1)

    console.print(
        Panel(
            f"  [bold green]🤖 FORGE Telegram Bot[/bold green]\n\n"
            f"  Token: [dim]{settings.mask(settings.telegram_bot_token)}[/dim]\n"
            f"  Chat:  [dim]{settings.telegram_chat_id}[/dim]\n\n"
            f"  Open Telegram and send your bot [bold]/start[/bold]\n"
            f"  Then: [bold]/generate A recipe app with meal planning[/bold]\n\n"
            f"  Press [bold]Ctrl+C[/bold] to stop the bot.",
            border_style="green",
        )
    )

    bot = ForgeBot(settings)
    bot.run()


# ─────────────────────────────────────────────────────────────────────────────
# forge doctor
# ─────────────────────────────────────────────────────────────────────────────

@app.command("doctor", help="Test API keys and connections.")
def doctor():
    from forge.core.pipeline import Pipeline
    from forge.utils.ui import print_doctor_result

    settings = Settings.load()
    console.print("\n  [bold]🔍 Testing connections…[/bold]")

    results = asyncio.run(Pipeline(settings).validate_keys())

    if settings.telegram_bot_token:
        results["telegram"] = True  # Telegram key present
    else:
        results["telegram"] = False

    print_doctor_result(results)

    all_ok = all(results.values())
    if all_ok:
        console.print("  [green]✓ All systems operational.[/green]\n")
    else:
        failed = [k for k, v in results.items() if not v]
        console.print(f"  [red]✗ Issues: {', '.join(failed)}[/red]")
        console.print("  Run [cyan]forge config[/cyan] to update keys.\n")
        raise typer.Exit(1)


# ─────────────────────────────────────────────────────────────────────────────
# forge refine
# ─────────────────────────────────────────────────────────────────────────────

@app.command("refine", help="Surgically update an existing FORGE project.")
def refine(
    project: Annotated[Path, typer.Argument(help="Path to the generated project directory")],
    request: Annotated[str, typer.Argument(help='What to change e.g. "Add push notifications"')],
    groq_key: Annotated[Optional[str], typer.Option("--groq-key", envvar="GROQ_API_KEY")] = None,
    nvidia_key: Annotated[Optional[str], typer.Option("--nvidia-key", envvar="NVIDIA_API_KEY")] = None,
    skip_ts_verify: Annotated[bool, typer.Option("--skip-verify")] = False,
    debug: Annotated[bool, typer.Option("--debug", hidden=True)] = False,
):
    from forge.utils.ui import print_banner, log_line, print_agent_header
    from forge.core.pipeline import Pipeline
    from forge.core.session import LogEntry, AgentRun, AgentStatus

    print_banner()
    if not project.exists():
        console.print(f"  [red]✗[/red] Project not found: {project}\n")
        raise typer.Exit(1)

    settings = Settings.load()
    if groq_key:
        settings.groq_api_key = groq_key
    if nvidia_key:
        settings.nvidia_api_key = nvidia_key
    settings.skip_ts_verify = skip_ts_verify

    issues = settings.validate_keys()
    if issues:
        for issue in issues:
            console.print(f"  [red]✗[/red] {issue}")
        raise typer.Exit(1)

    console.print(f"\n  Project: [bold]{project}[/bold]")
    console.print(f"  Request: [bold yellow]{request}[/bold yellow]\n")

    pipeline = Pipeline(settings)

    def on_log(entry: LogEntry) -> None:
        log_line(entry)

    def on_agent_update(run: AgentRun) -> None:
        if run.status == AgentStatus.RUNNING:
            print_agent_header(run.name, run.engine)

    try:
        result = asyncio.run(
            pipeline.refine(
                project_dir=project.resolve(),
                refine_request=request,
                on_log=on_log,
                on_agent_update=on_agent_update,
            )
        )
        if result.success:
            console.print(f"\n  [green]✓ Refine complete[/green] — {len(result.changed_files)} file(s) changed")
            if result.skipped_user_edits:
                console.print(f"  Protected user edits: {', '.join(result.skipped_user_edits)}", style="yellow")
            if result.zip_path:
                console.print(f"  New zip: {result.zip_path}\n")
        else:
            console.print(f"\n  [red]✗ Refine failed: {result.error}[/red]\n")
            raise typer.Exit(1)
    except KeyboardInterrupt:
        console.print("\n  Cancelled.\n")
        raise typer.Exit(130)
    except Exception as e:
        console.print(f"\n  [red]✗ ERROR: {e}[/red]\n")
        if debug:
            import traceback; traceback.print_exc()
        raise typer.Exit(1)


# ─────────────────────────────────────────────────────────────────────────────
# forge audit
# ─────────────────────────────────────────────────────────────────────────────

@app.command("audit", help="Run npm security audit + AI-powered fix suggestions.")
def audit_cmd(
    project: Annotated[Path, typer.Argument(help="Path to the project directory")],
    no_fix: Annotated[bool, typer.Option("--no-fix", help="Report only, don't auto-apply fixes")] = False,
):
    from forge.agents.auditor import run_audit
    from forge.core.session import GenerationSession
    from forge.core.api import GroqClient
    from forge.utils.ui import log_line

    settings = Settings.load()
    session = GenerationSession(prompt="audit", settings=settings)
    session.project_dir = project.resolve()
    session.on_log = log_line

    groq = GroqClient(settings.groq_api_key, timeout=60) if settings.groq_api_key else None

    console.print(f"\n  Auditing: [bold]{project}[/bold]\n")
    result = asyncio.run(run_audit(session, groq, auto_fix=not no_fix))

    console.print(f"\n  Total: {result.total} | Critical: {result.critical} | High: {result.high}")
    if result.applied_fixes:
        console.print(f"  [green]Applied: {', '.join(result.applied_fixes)}[/green]")
    console.print()


# ─────────────────────────────────────────────────────────────────────────────
# forge backend
# ─────────────────────────────────────────────────────────────────────────────

@app.command("backend", help="Add Supabase/Firebase/Appwrite backend to existing project.")
def backend_cmd(
    project: Annotated[Path, typer.Argument(help="Path to the project directory")],
    provider: Annotated[str, typer.Argument(help="supabase | firebase | appwrite")] = "supabase",
):
    from forge.agents.backend import run_backend_generator
    from forge.core.session import GenerationSession, AppPlan
    from forge.core.api import GroqClient
    from forge.utils.ui import log_line
    import json as _json

    if provider not in ("supabase", "firebase", "appwrite"):
        console.print(f"  [red]✗[/red] Unknown provider '{provider}'. Use: supabase | firebase | appwrite\n")
        raise typer.Exit(1)

    settings = Settings.load()
    issues = settings.validate_keys()
    if issues:
        for i in issues:
            console.print(f"  [red]✗[/red] {i}")
        raise typer.Exit(1)

    project_dir = project.resolve()
    session = GenerationSession(prompt="backend", settings=settings)
    session.project_dir = project_dir
    session.on_log = log_line

    plan_path = project_dir / ".forge" / "plan.json"
    if plan_path.exists():
        session.plan = AppPlan.from_dict(_json.loads(plan_path.read_text()))
    else:
        session.plan = AppPlan(app_name=project_dir.name, slug=project_dir.name)

    groq = GroqClient(settings.groq_api_key, timeout=120)

    console.print(f"\n  Adding [bold]{provider}[/bold] backend to [bold]{project_dir.name}[/bold]…\n")
    files = asyncio.run(run_backend_generator(session, groq, backend=provider))

    console.print(f"\n  [green]✓ {len(files)} backend files written[/green]")
    console.print(f"  See [dim]{project_dir}/backend/{provider}/README.md[/dim] for deploy steps.\n")


# ─────────────────────────────────────────────────────────────────────────────
# forge cost
# ─────────────────────────────────────────────────────────────────────────────

@app.command("cost", help="Show token usage and cost report for the last generation.")
def cost_cmd(
    project: Annotated[Optional[Path], typer.Argument(help="Project dir containing .forge/cost.json")] = None,
):
    import json as _json

    cost_file = (project or Path(".")) / ".forge" / "cost.json"
    if not cost_file.exists():
        console.print("  [yellow]No cost report found. Run[/yellow] [cyan]forge generate[/cyan] [yellow]first.[/yellow]\n")
        raise typer.Exit(0)

    data = _json.loads(cost_file.read_text())
    t = Table(box=box.SIMPLE, title="Token Cost Report")
    t.add_column("Provider")
    t.add_column("Model")
    t.add_column("Calls", justify="right")
    t.add_column("Prompt tok", justify="right")
    t.add_column("Output tok", justify="right")
    t.add_column("Cost (USD)", justify="right", style="bold yellow")

    for m in data.get("models", {}).values():
        t.add_row(
            m["provider"].upper(), m["model"],
            str(m["calls"]),
            f"{m['prompt_tokens']:,}", f"{m['completion_tokens']:,}",
            f"${m['cost_usd']:.4f}",
        )
    t.add_section()
    t.add_row("TOTAL", "", str(data["total_calls"]),
              f"{data['total_prompt_tokens']:,}", f"{data['total_completion_tokens']:,}",
              f"${data['total_cost_usd']:.4f}")

    console.print("\n", t, "\n")


# ─────────────────────────────────────────────────────────────────────────────
# forge models
# ─────────────────────────────────────────────────────────────────────────────

@app.command("models", help="List available AI models.")
def models():
    settings = Settings.load()

    t = Table(title="Available Models", box=box.SIMPLE)
    t.add_column("Provider", style="dim")
    t.add_column("Model")
    t.add_column("Role")
    t.add_column("Active", justify="center")

    for m in settings.GROQ_MODELS:
        active = "✓" if m == settings.plan_model else ""
        t.add_row("[red]Groq[/red]", m, "Planning / Validation / Tests", f"[green]{active}[/green]")

    for m in settings.NVIDIA_MODELS:
        active = "✓" if m == settings.code_model else ""
        t.add_row("[green]NVIDIA NIM[/green]", m, "Code Generation", f"[green]{active}[/green]")

    console.print("\n", t, "\n")


# ─────────────────────────────────────────────────────────────────────────────
# forge version
# ─────────────────────────────────────────────────────────────────────────────

@app.command("version", help="Show FORGE version.")
def version():
    console.print(f"\n  [bold yellow]FORGE AI[/bold yellow] v{__version__}\n")


# ─────────────────────────────────────────────────────────────────────────────
# Entry
# ─────────────────────────────────────────────────────────────────────────────

def app_entry():
    """PyPI entry point: forge command."""
    # First-run detection
    settings = Settings.load()
    if not settings.is_generation_ready():
        # Check if this is not just a config/help command
        if len(sys.argv) == 1 or (len(sys.argv) > 1 and sys.argv[1] not in ("config", "version", "--help", "-h", "--version")):
            from forge.utils.ui import print_banner
            print_banner()
            from rich.prompt import Confirm
            console.print(
                Panel(
                    "  [bold yellow]Welcome to FORGE![/bold yellow]\n\n"
                    "  It looks like this is your first run.\n"
                    "  Let's set up your API keys before generating apps.",
                    border_style="yellow",
                )
            )
            try:
                if Confirm.ask("  Run setup wizard now?", default=True):
                    from forge.utils.onboarding import run_onboarding
                    run_onboarding(Settings)
                    console.print()
            except (KeyboardInterrupt, EOFError):
                pass
    app()
