"""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.colony import colony_app
from fliiq.cli.daemon import daemon_app
from fliiq.cli.google import google_app
from fliiq.cli.jobs import job_app
from fliiq.cli.telegram import telegram_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: The AI agent you actually own")


@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    mode: str = typer.Option("autonomous", "--mode", help="Execution mode: autonomous, supervised, or plan"),
    provider: str = typer.Option(None, "--provider", help="LLM provider: anthropic, openai, or gemini"),
    model: str = typer.Option(None, "--model", "-m", help="Model alias or ID (e.g. opus-4.6, sonnet-4.5, gpt-4o)"),
    persona: str = typer.Option(None, "--persona", help="Activate a persona playbook (e.g. product-manager, frontend)"),
):
    """Fliiq: The AI agent you actually own."""
    _load_env()  # Configure logging + load .env for all commands
    from fliiq.runtime.update_check import trigger_update_check
    trigger_update_check()
    if ctx.invoked_subcommand is None:
        # No command given — launch interactive REPL
        _launch_repl(mode, provider=provider, model=model, persona=persona)


# 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(google_app, name="google")
app.add_typer(google_app, name="calendar", hidden=True)  # backward compat
app.add_typer(daemon_app, name="daemon")
app.add_typer(job_app, name="job")
app.add_typer(telegram_app, name="telegram")
app.add_typer(colony_app, name="colony")


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(provider: str | None = None, model: str | None = None):
    """Resolve LLM config. Wraps setup.resolve_llm_config with CLI error handling."""
    try:
        return resolve_llm_config(provider_override=provider, model_override=model)
    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, persona: str | None = None) -> 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 (persona flag takes priority over auto-detect)
    playbook_content = None
    if persona:
        playbook_content = load_playbook(persona, project_root=resources.project_root)
        if not playbook_content:
            display.print_error(f"Persona '{persona}' not found. Run `fliiq playbook list` to see available playbooks.")
            raise typer.Exit(1)
    else:
        domains = detect_domain(prompt, working_dir=resources.project_root, project_root=resources.project_root)
        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))

    # Enrich memory with prompt-relevant entities
    from fliiq.runtime.agent.setup import enrich_memory_context

    enriched_memory = enrich_memory_context(resources.memory_context, effective_prompt, resources.project_root)

    # 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=enriched_memory,
        user_profile=resources.user_profile,
        fliiq_email=resources.fliiq_email,
    )

    # 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, tool_input=tool_input)
        # 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)

    # Post-conversation memory extraction (non-fatal)
    if result.stop_reason != "cancelled" and result.iterations > 0:
        try:
            from fliiq.runtime.memory.extractor import extract_and_save_memories

            await extract_and_save_memories(resources.llm, result.messages, resources.project_root)
        except Exception:
            pass

    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,
            user_profile=resources.user_profile,
            fliiq_email=resources.fliiq_email,
        )
        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)

        # Post-conversation memory extraction (non-fatal)
        if result.stop_reason != "cancelled" and result.iterations > 0:
            try:
                from fliiq.runtime.memory.extractor import extract_and_save_memories

                await extract_and_save_memories(resources.llm, result.messages, resources.project_root)
            except Exception:
                pass

        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))

    # Cleanup MCP connections
    if resources.mcp_manager and hasattr(resources.mcp_manager, "disconnect_all"):
        await resources.mcp_manager.disconnect_all()


@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"),
    provider: str = typer.Option(None, "--provider", help="LLM provider: anthropic, openai, or gemini"),
    model: str = typer.Option(None, "--model", "-m", help="Model alias or ID (e.g. opus-4.6, sonnet-4.5, gpt-4o)"),
    persona: str = typer.Option(None, "--persona", help="Activate a persona playbook (e.g. product-manager, frontend)"),
    show_browser: bool = typer.Option(False, "--show-browser", help="Show browser window during web navigation tasks"),
):
    """Run a task using the agent loop."""
    if show_browser:
        os.environ["FLIIQ_SHOW_BROWSER"] = "1"
    llm_config = _resolve_llm_config(provider, model)
    asyncio.run(_run_agent(prompt, mode, no_reflect, llm_config, persona=persona))
    from fliiq.runtime.update_check import check_update_cached
    notice = check_update_cached()
    if notice:
        display.console.print(f"\n[dim]{notice}[/dim]")


@app.command()
def plan(
    prompt: str,
    no_reflect: bool = typer.Option(False, "--no-reflect", help="Skip pre-flight reflection"),
    provider: str = typer.Option(None, "--provider", help="LLM provider: anthropic, openai, or gemini"),
    model: str = typer.Option(None, "--model", "-m", help="Model alias or ID (e.g. opus-4.6, sonnet-4.5, gpt-4o)"),
    persona: str = typer.Option(None, "--persona", help="Activate a persona playbook (e.g. product-manager, frontend)"),
):
    """Generate a plan for a task (shortcut for `run --mode plan`)."""
    llm_config = _resolve_llm_config(provider, model)
    asyncio.run(_run_agent(prompt, "plan", no_reflect, llm_config, persona=persona))


@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, bundled_soul_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)
        (fliiq_dir / "playbooks").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)

        # Scaffold SOUL.md from template
        soul_path = fliiq_dir / "SOUL.md"
        if not soul_path.exists():
            template = bundled_soul_template()
            if template.exists():
                soul_path.write_text(template.read_text())

        # Scaffold mcp.json with empty servers
        mcp_path = fliiq_dir / "mcp.json"
        if not mcp_path.exists():
            import json

            mcp_path.write_text(json.dumps({"servers": {}}, indent=2) + "\n")

        typer.echo(f"Initialized project .fliiq/ at {fliiq_dir}")
        typer.echo("  SOUL.md    — customize agent personality")
        typer.echo("  playbooks/ — add domain-specific instructions")
        typer.echo("  mcp.json   — connect MCP servers")
        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"
            )

    # Create user.yaml from template if missing
    user_yaml_path = fliiq_dir / "user.yaml"
    if not user_yaml_path.exists():
        user_yaml_path.write_text(
            "# Your identity — loaded into every agent session so the agent knows who you are.\n"
            "# When you say \"my email\" or \"my calendar,\" the agent uses these accounts.\n"
            "#\n"
            "# name: Your Name\n"
            "# emails:\n"
            "#   - address: your@email.com\n"
            "#     label: personal\n"
            "#   - address: work@email.com\n"
            "#     label: work\n"
            "# timezone: America/New_York\n"
        )

    # Create models.yaml from defaults if missing
    models_path = fliiq_dir / "models.yaml"
    if not models_path.exists():
        models_path.write_text(
            "# Model aliases — shortcuts for --model flag\n"
            "# Usage: fliiq run \"task\" --model opus-4.6\n"
            "#\n"
            "# Any string not found here is passed through as-is to the provider.\n"
            "# Add your own aliases for Ollama, Mistral, DeepSeek, etc.\n"
            "#\n"
            "aliases:\n"
            "  # Anthropic\n"
            "  opus-4.6: claude-opus-4-6\n"
            "  sonnet-4.6: claude-sonnet-4-6\n"
            "  sonnet-4.0: claude-sonnet-4-20250514\n"
            "  haiku-4.5: claude-haiku-4-5-20251001\n"
            "\n"
            "  # OpenAI\n"
            "  gpt-4.1: gpt-4.1\n"
            "  gpt-4.1-mini: gpt-4.1-mini\n"
            "  gpt-4o: gpt-4o\n"
            "  gpt-4o-mini: gpt-4o-mini\n"
            "  o3: o3\n"
            "  o3-mini: o3-mini\n"
            "  o4-mini: o4-mini\n"
            "\n"
            "  # Gemini\n"
            "  gemini-2.5-pro: gemini-2.5-pro\n"
            "  gemini-2.5-flash: gemini-2.5-flash\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(f"Edit {user_yaml_path} to set your identity.")
    typer.echo("Then run `fliiq chat` from any terminal.")


@app.command()
def models():
    """List available model aliases and current default."""
    import yaml

    from fliiq.runtime.config import global_fliiq_dir
    from fliiq.runtime.llm.providers import DEFAULT_MODELS, LLMProvider

    models_file = global_fliiq_dir() / "models.yaml"
    if not models_file.is_file():
        typer.echo(f"No models.yaml found at {models_file}")
        typer.echo("Run `fliiq init` to create one with default aliases.")
        typer.echo("Or create it manually with your own aliases.")
        return

    try:
        data = yaml.safe_load(models_file.read_text()) or {}
    except Exception as e:
        display.print_error(f"Failed to parse {models_file}: {e}")
        raise typer.Exit(1)

    aliases = data.get("aliases", {})
    if not aliases:
        typer.echo(f"No aliases defined in {models_file}")
        return

    typer.echo(f"\nModel aliases ({models_file}):\n")
    max_alias = max(len(a) for a in aliases)
    for alias, model_id in aliases.items():
        typer.echo(f"  {alias:<{max_alias}}  ->  {model_id}")

    # Show current default
    default_model = DEFAULT_MODELS.get(LLMProvider.ANTHROPIC, "unknown")
    typer.echo(f"\nCurrent default: {default_model} (anthropic)")
    typer.echo("\nUsage: fliiq run \"task\" --model opus-4.6")
    typer.echo(f"       fliiq run \"task\" --model {default_model}")
    typer.echo(f"\nEdit {models_file} to add or change aliases.")


@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. User profile
    user_yaml = gdir / "user.yaml"
    if user_yaml.exists():
        from fliiq.runtime.config import load_user_profile

        profile = load_user_profile()
        if profile.get("name"):
            display.console.print(f"[green]OK[/green]  User profile: {profile['name']} ({user_yaml})")
        else:
            display.console.print(f"[yellow]WARN[/yellow]  User profile exists but is empty — edit {user_yaml}")
    else:
        display.console.print(f"[dim]SKIP[/dim]  No user profile (edit {user_yaml} to set your identity)")

    # 7. Optional features
    # Fliiq bot email (check new name first, fall back to old)
    fliiq_email = os.environ.get("FLIIQ_GMAIL_ADDRESS") or os.environ.get("GMAIL_ADDRESS")
    if fliiq_email:
        display.console.print(f"[green]OK[/green]  Fliiq bot email: {fliiq_email}")
    else:
        display.console.print(
            "[dim]SKIP[/dim]  Fliiq bot email not configured"
            " (optional — set FLIIQ_GMAIL_ADDRESS in .env)"
        )

    optional = [
        ("Twilio", "TWILIO_ACCOUNT_SID"),
        ("Telegram", "TELEGRAM_BOT_TOKEN"),
        ("Google OAuth", "GOOGLE_CLIENT_ID"),
    ]
    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)")

    # Google OAuth token check
    from fliiq.runtime.google_auth import TOKENS_PATH as google_tokens_path

    if os.environ.get("GOOGLE_CLIENT_ID"):
        if google_tokens_path.exists():
            import json

            try:
                accounts = list(json.loads(google_tokens_path.read_text()).keys())
                display.console.print(
                    f"[green]OK[/green]  Google OAuth authorized: {', '.join(accounts)}"
                )
            except Exception:
                display.console.print("[yellow]WARN[/yellow]  Google token file exists but is malformed")
        else:
            display.console.print(
                "[yellow]WARN[/yellow]  Google OAuth credentials set but not authorized. "
                "Run `fliiq google auth`."
            )

    # 8. Project customizations
    if local:
        if (local / "SOUL.md").exists():
            display.console.print(f"[green]OK[/green]  Project SOUL.md override: {local / 'SOUL.md'}")
        else:
            display.console.print("[dim]SKIP[/dim]  No project SOUL.md override (run `fliiq soul edit` to customize)")

        playbooks_dir = local / "playbooks"
        if playbooks_dir.is_dir():
            pb_count = len(list(playbooks_dir.glob("*.md")))
            if pb_count:
                display.console.print(f"[green]OK[/green]  Custom playbooks: {pb_count}")
            else:
                display.console.print("[dim]SKIP[/dim]  No custom playbooks (run `fliiq playbook create <name>`)")
        else:
            display.console.print("[dim]SKIP[/dim]  No playbooks/ directory")

        mcp_path = local / "mcp.json"
        if mcp_path.exists():
            import json

            try:
                mcp_data = json.loads(mcp_path.read_text())
                server_count = len(mcp_data.get("servers", {}))
                if server_count:
                    display.console.print(f"[green]OK[/green]  MCP servers: {server_count} configured")
                else:
                    display.console.print(
                        "[dim]SKIP[/dim]  MCP config exists but no servers"
                        " (run `fliiq mcp add <name>`)"
                    )
            except json.JSONDecodeError:
                display.console.print("[red]ERROR[/red]  mcp.json is invalid JSON")
                issues.append("Fix .fliiq/mcp.json — invalid JSON.")
        else:
            display.console.print("[dim]SKIP[/dim]  No MCP config (create .fliiq/mcp.json to add MCP servers)")

    # 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.package_data import bundled_skills_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 = bundled_skills_dir() / 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("edit")
def soul_edit():
    """Open project SOUL.md for editing. Creates from template if missing."""
    import subprocess

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

    try:
        fliiq_dir = resolve_fliiq_dir()
    except FileNotFoundError:
        display.print_error("No .fliiq/ directory found. Run `fliiq init` first.")
        raise typer.Exit(1)

    soul_path = fliiq_dir / "SOUL.md"
    if not soul_path.exists():
        template = bundled_soul_template()
        if template.exists():
            soul_path.write_text(template.read_text())
        else:
            soul_path.write_text("# Project Identity Overrides\n\n")
        typer.echo(f"Created {soul_path} from template.")

    editor = os.environ.get("EDITOR", "vim")
    subprocess.call([editor, str(soul_path)])


@soul_app.command("reset")
def soul_reset():
    """Remove project SOUL.md overrides (revert to defaults)."""
    from fliiq.runtime.config import resolve_fliiq_dir

    try:
        fliiq_dir = resolve_fliiq_dir()
    except FileNotFoundError:
        display.print_error("No .fliiq/ directory found.")
        raise typer.Exit(1)

    soul_path = fliiq_dir / "SOUL.md"
    if soul_path.exists():
        soul_path.unlink()
        typer.echo(f"Removed {soul_path}. Using default SOUL.md.")
    else:
        typer.echo("No project SOUL.md overrides to reset.")


# --- Playbook commands ---


@playbook_app.command("list")
def playbook_list():
    """List available playbooks with source."""
    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()

    found: dict[str, str] = {}

    # Custom playbooks (highest priority)
    custom_dir = root / ".fliiq" / "playbooks"
    if custom_dir.is_dir():
        for pb in sorted(custom_dir.glob("*.md")):
            found[pb.stem] = "custom"

    # Project playbooks
    project_dir = root / "playbooks"
    if project_dir.is_dir():
        for pb in sorted(project_dir.glob("*.md")):
            if pb.stem not in found:
                found[pb.stem] = "project"

    # Bundled
    bundled = bundled_playbooks_dir()
    if bundled.is_dir():
        for pb in sorted(bundled.glob("*.md")):
            if pb.stem not in found:
                found[pb.stem] = "bundled"

    if not found:
        typer.echo("No playbooks found. Run `fliiq playbook create <name>` to create one.")
        return

    for name, source in sorted(found.items()):
        typer.echo(f"  {name} ({source})")


@playbook_app.command("create")
def playbook_create(name: str):
    """Create a new custom playbook in .fliiq/playbooks/."""
    from fliiq.runtime.config import resolve_fliiq_dir
    from fliiq.runtime.package_data import bundled_playbook_template

    try:
        fliiq_dir = resolve_fliiq_dir()
    except FileNotFoundError:
        display.print_error("No .fliiq/ directory found. Run `fliiq init` first.")
        raise typer.Exit(1)

    playbooks_dir = fliiq_dir / "playbooks"
    playbooks_dir.mkdir(exist_ok=True)

    pb_path = playbooks_dir / f"{name}.md"
    if pb_path.exists():
        typer.echo(f"Playbook '{name}' already exists at {pb_path}")
        return

    template = bundled_playbook_template()
    content = template.read_text().replace("{domain}", name) if template.exists() else f"# {name} Playbook\n\n"
    pb_path.write_text(content)

    typer.echo(f"Created {pb_path}")
    typer.echo(f"Edit it, then Fliiq will load it when it detects '{name}'-related tasks.")


@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)


# --- MCP commands ---

mcp_app = typer.Typer(help="Manage MCP server connections")
app.add_typer(mcp_app, name="mcp")


@mcp_app.command("list")
def mcp_list():
    """List configured MCP servers."""
    from fliiq.runtime.config import resolve_fliiq_dir
    from fliiq.runtime.mcp.client import load_mcp_config

    try:
        root = resolve_fliiq_dir().parent
    except FileNotFoundError:
        root = Path.cwd()

    config = load_mcp_config(root)
    servers = config.get("servers", {})

    if not servers:
        typer.echo("No MCP servers configured.")
        typer.echo("Run `fliiq mcp add <name> --command <cmd>` or `fliiq mcp add <name> --url <url>`")
        return

    for name, cfg in servers.items():
        transport = cfg.get("transport", "stdio")
        if transport == "streamable-http":
            target = cfg.get("url", "?")
        else:
            target = f"{cfg.get('command', '?')} {' '.join(cfg.get('args', []))}"
        typer.echo(f"  {name} ({transport}): {target}")


@mcp_app.command("add")
def mcp_add(
    name: str,
    command: str = typer.Option(None, "--command", help="Command for stdio transport (e.g. npx, uvx)"),
    args: str = typer.Option(None, "--args", help="Space-separated args for stdio command"),
    url: str = typer.Option(None, "--url", help="URL for streamable-http transport"),
):
    """Add an MCP server. Use --command for stdio or --url for streamable-http."""
    from fliiq.runtime.config import resolve_fliiq_dir
    from fliiq.runtime.mcp.client import load_mcp_config, save_mcp_config

    if not command and not url:
        display.print_error("Provide --command (stdio) or --url (streamable-http).")
        raise typer.Exit(1)

    if command and url:
        display.print_error("Provide --command OR --url, not both.")
        raise typer.Exit(1)

    try:
        root = resolve_fliiq_dir().parent
    except FileNotFoundError:
        display.print_error("No .fliiq/ directory found. Run `fliiq init` first.")
        raise typer.Exit(1)

    config = load_mcp_config(root)
    if "servers" not in config:
        config["servers"] = {}

    if name in config["servers"]:
        display.print_error(f"Server '{name}' already exists. Remove it first with `fliiq mcp remove {name}`.")
        raise typer.Exit(1)

    if url:
        config["servers"][name] = {"url": url, "transport": "streamable-http"}
    else:
        server_args = args.split() if args else []
        config["servers"][name] = {"command": command, "args": server_args, "transport": "stdio"}

    save_mcp_config(root, config)
    typer.echo(f"Added MCP server '{name}'. Run `fliiq mcp test {name}` to verify.")


@mcp_app.command("remove")
def mcp_remove(name: str):
    """Remove an MCP server."""
    from fliiq.runtime.config import resolve_fliiq_dir
    from fliiq.runtime.mcp.client import load_mcp_config, save_mcp_config

    try:
        root = resolve_fliiq_dir().parent
    except FileNotFoundError:
        display.print_error("No .fliiq/ directory found.")
        raise typer.Exit(1)

    config = load_mcp_config(root)
    servers = config.get("servers", {})

    if name not in servers:
        display.print_error(f"Server '{name}' not found.")
        raise typer.Exit(1)

    del servers[name]
    save_mcp_config(root, config)
    typer.echo(f"Removed MCP server '{name}'.")


@mcp_app.command("test")
def mcp_test(
    server: str = typer.Argument(None, help="Server name to test (all if omitted)"),
):
    """Test MCP server connections and list their tools."""
    from fliiq.runtime.config import resolve_fliiq_dir
    from fliiq.runtime.mcp.client import MCPManager, load_mcp_config

    try:
        root = resolve_fliiq_dir().parent
    except FileNotFoundError:
        display.print_error("No .fliiq/ directory found.")
        raise typer.Exit(1)

    config = load_mcp_config(root)
    servers = config.get("servers", {})

    if not servers:
        typer.echo("No MCP servers configured.")
        return

    if server:
        if server not in servers:
            display.print_error(f"Server '{server}' not found.")
            raise typer.Exit(1)
        to_test = {server: servers[server]}
    else:
        to_test = servers

    async def _test():
        manager = MCPManager()
        for name, cfg in to_test.items():
            typer.echo(f"Testing {name}...")
            result = await manager.test_server(name, cfg)
            if result["status"] == "ok":
                tools_str = ", ".join(result["tools"])
                display.console.print(f"  [green]OK[/green]  {len(result['tools'])} tool(s): {tools_str}")
            else:
                display.console.print(f"  [red]FAIL[/red]  {result['error']}")

    asyncio.run(_test())


# --- Interactive commands ---


def _launch_repl(mode: str, provider: str | None = None, model: str | None = None, persona: str | None = None):
    """Launch the interactive REPL."""
    llm_config = _resolve_llm_config(provider, model)
    from fliiq.cli.repl import ChatSession, _repl_ask_user_choice, _repl_plan_complete, run_repl

    async def _start():
        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,
            user_profile=resources.user_profile,
            fliiq_email=resources.fliiq_email,
            persona=persona,
        )
        try:
            await run_repl(session)
        finally:
            if resources.mcp_manager and hasattr(resources.mcp_manager, "disconnect_all"):
                await resources.mcp_manager.disconnect_all()

    asyncio.run(_start())


@app.command()
def tui(
    mode: str = typer.Option("autonomous", "--mode", help="Execution mode: autonomous, supervised, or plan"),
    provider: str = typer.Option(None, "--provider", help="LLM provider: anthropic, openai, or gemini"),
    model: str = typer.Option(None, "--model", "-m", help="Model alias or ID (e.g. opus-4.6, sonnet-4.5, gpt-4o)"),
    persona: str = typer.Option(None, "--persona", help="Activate a persona playbook (e.g. product-manager, frontend)"),
):
    """Start a full-screen TUI chat session."""
    llm_config = _resolve_llm_config(provider, model)
    from fliiq.cli.tui.app import run_tui

    async def _start():
        resources = await _setup_agent_resources(mode, llm_config)
        try:
            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,
                user_profile=resources.user_profile,
                fliiq_email=resources.fliiq_email,
                persona=persona,
            )
        finally:
            if resources.mcp_manager and hasattr(resources.mcp_manager, "disconnect_all"):
                await resources.mcp_manager.disconnect_all()

    asyncio.run(_start())


if __name__ == "__main__":
    app()
