"""Fliiq CLI — entry point for all commands."""

import asyncio
import os
from pathlib import Path
from typing import Callable

import structlog
import typer

from fliiq.cli import display
from fliiq.cli.daemon import daemon_app
from fliiq.cli.jobs import job_app
from fliiq.runtime.agent.config import AgentConfig
from fliiq.runtime.agent.setup import AgentResources, resolve_llm_config, setup_agent_resources

log = structlog.get_logger()

app = typer.Typer(help="Fliiq: Developer-first AI agent")


@app.callback(invoke_without_command=True)
def main(ctx: typer.Context):
    """Fliiq: Developer-first AI agent."""
    _load_env()  # Configure logging + load .env for all commands
    if ctx.invoked_subcommand is None:
        # No command given — launch interactive TUI
        ctx.invoke(chat, mode="autonomous", no_reflect=False, legacy=False)


# Sub-apps
soul_app = typer.Typer(help="Manage agent identity (SOUL.md)")
playbook_app = typer.Typer(help="Manage domain playbooks")
app.add_typer(soul_app, name="soul")
app.add_typer(playbook_app, name="playbook")

app.add_typer(daemon_app, name="daemon")
app.add_typer(job_app, name="job")


def _load_env():
    """Auto-load .env and configure logging."""
    import logging

    from dotenv import load_dotenv

    from fliiq.runtime.config import resolve_env_file

    env_path = resolve_env_file()
    if env_path:
        load_dotenv(env_path)

    # Default to WARNING (hide debug/info), enable debug with FLIIQ_DEBUG=1
    log_level = logging.DEBUG if os.environ.get("FLIIQ_DEBUG") else logging.WARNING

    # Configure stdlib logging (for httpx, etc.)
    logging.basicConfig(level=log_level)

    structlog.configure(
        wrapper_class=structlog.make_filtering_bound_logger(log_level),
    )


def _resolve_llm_config():
    """Resolve LLM config from env vars. Wraps setup.resolve_llm_config with CLI error handling."""
    try:
        return resolve_llm_config()
    except RuntimeError as e:
        display.print_error(str(e))
        raise typer.Exit(1)


# --- Core commands ---


def _cli_ask_user(question: str) -> str:
    """CLI callback for the ask_user tool — prompts user via terminal."""
    display.print_markdown(f"\n**Agent asks:** {question}")
    return typer.prompt("  Your answer")


def _cli_ask_user_choice(question: str, options: list[dict], recommended: int) -> str:
    """CLI callback for ask_user_choice — presents numbered options in terminal."""
    display.print_markdown(f"\n**Agent asks:** {question}")
    for i, opt in enumerate(options, 1):
        rec = " (recommended)" if i == recommended else ""
        display.console.print(f"  [{i}] {opt['label']}{rec} — {opt['description']}")
    display.console.print("  [0] Custom answer")

    while True:
        answer = typer.prompt("  Your choice (number or custom text)")
        try:
            choice = int(answer)
            if choice == 0:
                custom = typer.prompt("  Your custom answer")
                return f"User chose: {custom}"
            if 1 <= choice <= len(options):
                picked = options[choice - 1]
                return f"User chose: {picked['label']} — {picked['description']}"
            display.console.print(f"  [yellow]Pick 0-{len(options)}[/yellow]")
        except ValueError:
            return f"User chose: {answer}"


def _cli_plan_complete(summary: str, options: list[dict], recommended: int) -> str:
    """CLI callback for plan_complete — presents numbered options + free-form."""
    display.print_markdown(f"\n**Plan Summary:**\n{summary}")
    for i, opt in enumerate(options, 1):
        rec = " (recommended)" if i == recommended else ""
        display.console.print(f"  [{i}] {opt['label']}{rec} — {opt['description']}")
    display.console.print(f"  [{len(options) + 1}] Type your own response")

    while True:
        try:
            answer = typer.prompt("  Your choice").strip()
        except (KeyboardInterrupt, EOFError):
            display.console.print("\n[dim]Rejected.[/dim]")
            return "rejected"
        try:
            choice = int(answer)
            if choice == len(options) + 1:
                try:
                    custom = typer.prompt("  Your response").strip()
                except (KeyboardInterrupt, EOFError):
                    display.console.print("\n[dim]Rejected.[/dim]")
                    return "rejected"
                return f"revise: {custom}"
            if 1 <= choice <= len(options):
                opt = options[choice - 1]
                if opt.get("action") == "execute":
                    return "approved"
                return f"User chose: {opt['label']} — {opt['description']}"
        except ValueError:
            pass
        typer.echo(f"  Pick 1-{len(options) + 1}.")


async def _setup_agent_resources(
    mode: str,
    llm_config,
    project_root: Path | None = None,
    ask_user_handler: Callable | None = None,
    ask_user_choice_handler: Callable | None = None,
    plan_complete_handler: Callable | None = None,
) -> AgentResources:
    """CLI wrapper around setup_agent_resources — adds typer error handling."""
    try:
        return await setup_agent_resources(
            mode=mode,
            llm_config=llm_config,
            project_root=project_root,
            ask_user_handler=ask_user_handler or _cli_ask_user,
            ask_user_choice_handler=ask_user_choice_handler or _cli_ask_user_choice,
            plan_complete_handler=plan_complete_handler,
        )
    except (FileNotFoundError, RuntimeError) as e:
        display.print_error(str(e))
        raise typer.Exit(1)


async def _run_agent(prompt: str, mode: str, no_reflect: bool, llm_config) -> None:
    """Shared agent loop runner for `run` and `plan` commands."""
    import signal

    from fliiq.runtime.agent.audit import detect_confabulation, extract_audit_trail
    from fliiq.runtime.agent.loop import CancellationToken, agent_loop, plan_was_approved
    from fliiq.runtime.agent.prompt import assemble_agent_prompt
    from fliiq.runtime.planning.domain_detector import detect_domain
    from fliiq.runtime.planning.playbook_loader import load_playbook
    from fliiq.runtime.planning.reflection import reflect

    # Show mode at start
    display.print_mode(mode)

    # Setup resources (LLM, tools, soul, memory)
    resources = await _setup_agent_resources(
        mode, llm_config,
        plan_complete_handler=_cli_plan_complete,
    )

    # Detect domain + load playbook (prompt-dependent)
    domains = detect_domain(prompt, working_dir=resources.project_root)
    playbook_content = None
    for domain in domains:
        pb = load_playbook(domain, project_root=resources.project_root)
        if pb:
            playbook_content = pb
            break

    # Pre-flight reflection (with memory context)
    clarifications_text = None
    effective_prompt = prompt
    if not no_reflect:
        try:
            from fliiq.runtime.planning.prompt_assembly import assemble_system_prompt

            reflect_system = assemble_system_prompt(soul=resources.soul, user_soul=resources.user_soul)
            if resources.memory_context:
                reflect_system += f"\n\n## Memory Context\n\n{resources.memory_context}"
            ready, questions = await reflect(resources.llm, prompt, reflect_system)

            if not ready and questions:
                display.print_markdown("\n**Clarification needed:**\n")
                answers = []
                for q in questions:
                    display.print_markdown(f"  {q}")
                    answer = typer.prompt("  Answer")
                    answers.append(answer)
                clarifications_text = "\n".join(f"Q: {q}\nA: {a}" for q, a in zip(questions, answers))
                effective_prompt = f"{prompt}\n\nClarifications:\n{clarifications_text}"
        except Exception as e:
            log.debug("reflection_skipped", error=str(e))

    # Assemble system prompt
    system = assemble_agent_prompt(
        soul=resources.soul,
        user_soul=resources.user_soul,
        playbook=playbook_content,
        skills=resources.skill_info if (resources.skill_info and mode != "plan") else None,
        mode=mode,
        clarifications=clarifications_text,
        memory_context=resources.memory_context,
    )

    # Run agent loop
    config = AgentConfig(mode=mode)
    messages = [{"role": "user", "content": effective_prompt}]

    # Filter tool definitions by mode
    BUILTIN_TOOLS = {"todo", "ask_user", "ask_user_choice", "plan_complete"}
    all_defs = resources.tools.get_tool_definitions()
    if mode == "plan":
        tool_defs_override = [t for t in all_defs if t["name"] in BUILTIN_TOOLS]
    else:
        tool_defs_override = [t for t in all_defs if t["name"] != "plan_complete"]

    # Supervised mode: wire approval callback
    approve_all = False

    def _on_before_tool_call(name: str, tool_input: dict) -> bool:
        nonlocal approve_all
        if approve_all:
            return True
        response = display.prompt_tool_approval(name, tool_input)
        if response == "a":
            approve_all = True
            return True
        return response == "y"

    def _on_tool_call(name, tool_input, result):
        display.print_tool_call(name, result)
        # Refresh skill_info after a successful skill installation
        if name == "install_skill" and "installed successfully" in str(result):
            resources.skill_info = [
                {"name": d["name"], "description": d["description"]}
                for d in resources.tools.get_tool_definitions()
            ]

    # Set up Ctrl+C cancellation (double Ctrl+C to force)
    cancellation = CancellationToken()
    original_handler = signal.getsignal(signal.SIGINT)
    sigint_count = 0

    def _sigint_handler(signum, frame):
        nonlocal sigint_count
        sigint_count += 1
        cancellation.cancel()
        if sigint_count >= 2:
            raise KeyboardInterrupt
        display.console.print("\n[dim]Cancelling... (Ctrl+C again to force)[/dim]")

    signal.signal(signal.SIGINT, _sigint_handler)

    try:
        result = await agent_loop(
            llm=resources.llm,
            messages=messages,
            tools=resources.tools,
            config=config,
            system=system,
            on_tool_call=_on_tool_call,
            on_before_tool_call=(
                _on_before_tool_call if mode == "supervised"
                else (lambda name, _: name in BUILTIN_TOOLS) if mode == "plan"
                else None
            ),
            cancellation=cancellation,
            tool_defs_override=tool_defs_override,
            tool_skip_message=(
                "BLOCKED: tool unavailable in plan mode. "
                "Use only: todo, ask_user, ask_user_choice, plan_complete."
                if mode == "plan" else "Tool skipped by user"
            ),
        )
    except Exception as e:
        display.print_error(f"Agent loop failed: {e}")
        raise typer.Exit(1)
    finally:
        signal.signal(signal.SIGINT, original_handler)

    if result.stop_reason == "cancelled":
        display.console.print("[dim]Cancelled.[/dim]")

    # Display final output
    if result.final_text:
        display.console.print()  # blank line
        display.print_markdown(result.final_text)

    # Plan mode: auto-continue in supervised mode on approval
    if mode == "plan" and plan_was_approved(result):
        resources.tools._mode = "supervised"
        resources.tools._tools.pop("plan_complete", None)
        display.print_mode("supervised")
        display.console.print("[dim]Plan approved. Executing...[/dim]")

        messages.append({"role": "user", "content": "Plan approved. Proceed with execution."})
        system = assemble_agent_prompt(
            soul=resources.soul,
            user_soul=resources.user_soul,
            playbook=playbook_content,
            skills=resources.skill_info if resources.skill_info else None,
            mode="supervised",
            memory_context=resources.memory_context,
        )
        config = AgentConfig(mode="supervised")

        signal.signal(signal.SIGINT, _sigint_handler)
        try:
            result = await agent_loop(
                llm=resources.llm,
                messages=messages,
                tools=resources.tools,
                config=config,
                system=system,
                on_tool_call=_on_tool_call,
                on_before_tool_call=_on_before_tool_call,
                cancellation=cancellation,
            )
        except Exception as e:
            display.print_error(f"Agent loop failed: {e}")
            raise typer.Exit(1)
        finally:
            signal.signal(signal.SIGINT, original_handler)

        if result.stop_reason == "cancelled":
            display.console.print("[dim]Cancelled.[/dim]")
        elif result.final_text:
            display.console.print()
            display.print_markdown(result.final_text)

    # Check for confabulation warnings (audit trail saved automatically by agent_loop)
    try:
        trail = extract_audit_trail(prompt, result.messages, stop_reason=result.stop_reason)
        confab_warnings = detect_confabulation(trail)
        if confab_warnings:
            for w in confab_warnings:
                display.print_error(w)
    except Exception as e:
        log.warning("confabulation_check_failed", error=str(e))


@app.command()
def run(
    prompt: str,
    mode: str = typer.Option("autonomous", "--mode", help="Execution mode: autonomous, supervised, or plan"),
    no_reflect: bool = typer.Option(False, "--no-reflect", help="Skip pre-flight reflection"),
):
    """Run a task using the agent loop."""
    llm_config = _resolve_llm_config()
    asyncio.run(_run_agent(prompt, mode, no_reflect, llm_config))


@app.command()
def plan(
    prompt: str,
    no_reflect: bool = typer.Option(False, "--no-reflect", help="Skip pre-flight reflection"),
):
    """Generate a plan for a task (shortcut for `run --mode plan`)."""
    llm_config = _resolve_llm_config()
    asyncio.run(_run_agent(prompt, "plan", no_reflect, llm_config))


@app.command()
def init(
    project: bool = typer.Option(False, "--project", help="Initialize a project-local .fliiq/ in cwd"),
):
    """Initialize Fliiq. Default: global ~/.fliiq/. With --project: local .fliiq/ in cwd."""
    from fliiq.runtime.config import global_fliiq_dir
    from fliiq.runtime.memory.manager import MemoryManager
    from fliiq.runtime.package_data import bundled_env_template

    def _init_dir(fliiq_dir: Path, *, create_jobs: bool = False) -> None:
        fliiq_dir.mkdir(exist_ok=True)
        (fliiq_dir / "audit").mkdir(exist_ok=True)
        (fliiq_dir / "skills").mkdir(exist_ok=True)
        if create_jobs:
            (fliiq_dir / "jobs").mkdir(exist_ok=True)
        manager = MemoryManager(fliiq_dir / "memory")
        manager.ensure_dirs()

    if project:
        fliiq_dir = Path.cwd() / ".fliiq"
        if fliiq_dir.exists():
            typer.echo(f".fliiq/ already exists at {fliiq_dir}")
            return
        _init_dir(fliiq_dir, create_jobs=True)
        typer.echo(f"Initialized project .fliiq/ at {fliiq_dir}")
        return

    # Global init
    fliiq_dir = global_fliiq_dir()
    already_exists = fliiq_dir.exists()
    _init_dir(fliiq_dir)

    # Create .env from template if missing
    env_path = fliiq_dir / ".env"
    if not env_path.exists():
        template = bundled_env_template()
        if template.exists():
            env_path.write_text(template.read_text())
        else:
            env_path.write_text(
                "# LLM Provider API Keys (at least one required)\n"
                "ANTHROPIC_API_KEY=\nOPENAI_API_KEY=\nGEMINI_API_KEY=\n"
            )

    if already_exists:
        typer.echo(f"~/.fliiq/ already exists at {fliiq_dir}")
    else:
        typer.echo(f"Initialized ~/.fliiq/ at {fliiq_dir}")
    typer.echo(f"Edit {env_path} to add your API key.")
    typer.echo("Then run `fliiq chat` from any terminal.")


@app.command()
def doctor():
    """Verify Fliiq setup and diagnose issues."""
    from fliiq.runtime.config import global_fliiq_dir, local_fliiq_dir, resolve_env_file
    from fliiq.runtime.package_data import bundled_skills_dir, bundled_soul_path

    issues = []

    # 1. Global dir
    gdir = global_fliiq_dir()
    if gdir.is_dir():
        display.console.print(f"[green]OK[/green]  Global config: {gdir}")
    else:
        display.console.print(f"[red]MISSING[/red]  Global config: {gdir}")
        issues.append("Run `fliiq init` to create global config.")

    # 2. Local dir
    local = local_fliiq_dir()
    if local:
        display.console.print(f"[green]OK[/green]  Project config: {local}")
    else:
        display.console.print("[dim]SKIP[/dim]  No project .fliiq/ (optional — run `fliiq init --project`)")

    # 3. .env
    env = resolve_env_file()
    if env:
        display.console.print(f"[green]OK[/green]  .env file: {env}")
    else:
        display.console.print("[red]MISSING[/red]  No .env file found")
        issues.append("Run `fliiq init` or create .env with API keys.")

    # 4. LLM API key
    _load_env()
    has_key = any(os.environ.get(k) for k in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"])
    if has_key:
        provider = next(
            k.replace("_API_KEY", "").title()
            for k in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"]
            if os.environ.get(k)
        )
        display.console.print(f"[green]OK[/green]  LLM API key: {provider}")
    else:
        display.console.print("[red]MISSING[/red]  No LLM API key set")
        issues.append("Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY in your .env file.")

    # 5. Bundled assets
    if bundled_soul_path().exists():
        display.console.print("[green]OK[/green]  Bundled SOUL.md")
    else:
        display.console.print("[red]MISSING[/red]  Bundled SOUL.md not found")
        issues.append("Package data may be corrupted. Reinstall fliiq.")

    skills_dir = bundled_skills_dir()
    if skills_dir.is_dir():
        count = len(list(skills_dir.rglob("SKILL.md")))
        display.console.print(f"[green]OK[/green]  Bundled skills: {count}")
    else:
        display.console.print("[red]MISSING[/red]  Bundled skills not found")
        issues.append("Package data may be corrupted. Reinstall fliiq.")

    # 6. Optional features
    optional = [("Gmail", "GMAIL_ADDRESS"), ("Twilio", "TWILIO_ACCOUNT_SID"), ("Telegram", "TELEGRAM_BOT_TOKEN")]
    for label, key in optional:
        if os.environ.get(key):
            display.console.print(f"[green]OK[/green]  {label} configured")
        else:
            display.console.print(f"[dim]SKIP[/dim]  {label} not configured (optional)")

    # Summary
    display.console.print()
    if issues:
        display.console.print(f"[red]{len(issues)} issue(s):[/red]")
        for issue in issues:
            display.console.print(f"  - {issue}")
    else:
        display.console.print("[green]All checks passed.[/green]")


# --- Skill commands ---


@app.command("skill-list")
def skill_list():
    """List available skills with source (core/local)."""
    from fliiq.runtime.config import resolve_fliiq_dir
    from fliiq.runtime.skills.loader import discover_skills

    try:
        root = resolve_fliiq_dir().parent
    except FileNotFoundError:
        root = Path.cwd()
    skills = discover_skills(project_root=root)
    if not skills:
        display.print_error("No skills found.")
        return

    def _source(skill) -> str:
        d = skill._skill_dir
        if "/skills/core/" in d or d.endswith("/skills/core"):
            return "core"
        if "/fliiq/data/skills/core/" in d:
            return "core"
        if "/.fliiq/skills/" in d or d.endswith("/.fliiq/skills"):
            return "local"
        return "other"

    rows = [[name, _source(skill), skill.description] for name, skill in skills.items()]
    display.print_table(["Name", "Source", "Description"], rows, title="Available Skills")


@app.command("skill-promote")
def skill_promote(name: str):
    """Promote a local skill from .fliiq/skills/ to skills/core/."""
    import shutil

    from fliiq.runtime.config import local_fliiq_dir
    from fliiq.runtime.skills.installer import SkillInstallError, validate_skill_directory

    local = local_fliiq_dir()
    if not local:
        display.print_error("skill-promote requires a local .fliiq/ directory. Run `fliiq init --project` first.")
        raise typer.Exit(1)
    source = local / "skills" / name
    target = local.parent / "skills" / "core" / name

    if not source.is_dir():
        display.print_error(f"Local skill not found: {source}")
        raise typer.Exit(1)

    if target.exists():
        display.print_error(f"Core skill '{name}' already exists at {target}")
        raise typer.Exit(1)

    try:
        validate_skill_directory(source)
    except SkillInstallError as e:
        display.print_error(f"Validation failed: {e}")
        raise typer.Exit(1)

    shutil.copytree(source, target, ignore=shutil.ignore_patterns("__pycache__"))
    shutil.rmtree(source)
    display.print_markdown(f"Promoted **{name}** → `skills/core/{name}/`")


# --- Soul commands ---


@soul_app.command("show")
def soul_show():
    """Display current SOUL.md content."""
    from fliiq.runtime.planning.playbook_loader import load_soul

    try:
        from fliiq.runtime.config import resolve_fliiq_dir

        try:
            root = resolve_fliiq_dir().parent
        except FileNotFoundError:
            root = Path.cwd()
        soul, user_soul = load_soul(project_root=root)
    except FileNotFoundError as e:
        display.print_error(str(e))
        raise typer.Exit(1)

    display.print_markdown(soul)
    if user_soul:
        display.print_markdown("\n---\n\n**User Overrides (.fliiq/SOUL.md)**\n")
        display.print_markdown(user_soul)


@soul_app.command("reset")
def soul_reset():
    """Reset user SOUL.md overrides."""
    typer.echo("[Phase 2] Soul reset not yet implemented.")


# --- Playbook commands ---


@playbook_app.command("list")
def playbook_list():
    """List available playbooks."""
    from pathlib import Path

    from fliiq.runtime.config import resolve_fliiq_dir
    from fliiq.runtime.package_data import bundled_playbooks_dir

    try:
        root = resolve_fliiq_dir().parent
    except FileNotFoundError:
        root = Path.cwd()
    playbooks_dir = root / "playbooks"
    found = set()

    if playbooks_dir.is_dir():
        for pb in sorted(playbooks_dir.glob("*.md")):
            found.add(pb.stem)
            typer.echo(f"  {pb.stem}")

    bundled = bundled_playbooks_dir()
    if bundled.is_dir():
        for pb in sorted(bundled.glob("*.md")):
            if pb.stem not in found:
                typer.echo(f"  {pb.stem}")
                found.add(pb.stem)

    if not found:
        typer.echo("No playbooks found.")


@playbook_app.command("show")
def playbook_show(name: str):
    """Display a playbook's content."""
    from fliiq.runtime.config import resolve_fliiq_dir
    from fliiq.runtime.planning.playbook_loader import load_playbook

    try:
        root = resolve_fliiq_dir().parent
    except FileNotFoundError:
        root = Path.cwd()
    content = load_playbook(name, project_root=root)
    if content is None:
        display.print_error(f"Playbook '{name}' not found.")
        raise typer.Exit(1)

    display.print_markdown(content)


# --- Chat command ---


@app.command()
def chat(
    mode: str = typer.Option("autonomous", "--mode", help="Execution mode: autonomous, supervised, or plan"),
    no_reflect: bool = typer.Option(False, "--no-reflect", help="Skip pre-flight reflection (unused in chat)"),
    legacy: bool = typer.Option(False, "--legacy", help="Use legacy REPL instead of TUI"),
):
    """Start an interactive chat session with the agent."""
    llm_config = _resolve_llm_config()

    if legacy:
        # Legacy REPL mode
        from fliiq.cli.repl import ChatSession, _repl_ask_user_choice, _repl_plan_complete, run_repl

        async def _start_repl():
            resources = await _setup_agent_resources(
                mode, llm_config,
                ask_user_choice_handler=_repl_ask_user_choice,
                plan_complete_handler=_repl_plan_complete,
            )
            session = ChatSession(
                llm=resources.llm,
                tools=resources.tools,
                soul=resources.soul,
                user_soul=resources.user_soul,
                skill_info=resources.skill_info,
                memory_context=resources.memory_context,
                project_root=resources.project_root,
                mode=mode,
            )
            await run_repl(session)

        asyncio.run(_start_repl())
    else:
        # Full-screen TUI mode (default)
        from fliiq.cli.tui.app import run_tui

        async def _start_tui():
            resources = await _setup_agent_resources(mode, llm_config)
            await run_tui(
                llm=resources.llm,
                tools=resources.tools,
                soul=resources.soul,
                user_soul=resources.user_soul,
                skill_info=resources.skill_info,
                memory_context=resources.memory_context,
                project_root=resources.project_root,
                mode=mode,
            )

        asyncio.run(_start_tui())


if __name__ == "__main__":
    app()
