"""CLI entry point for Red Hat Oracle."""

from __future__ import annotations

import asyncio
import subprocess
import sys
import threading
from typing import Any

import click
from rich.console import Console
from rich.progress import Progress
from rich.table import Table

from redhat_oracle import __version__
from redhat_oracle.core.config import load_config

console = Console()


def _run_async(coro: Any) -> Any:
    """Run an async coroutine from synchronous CLI code using a background thread."""
    result: Any = None
    exception: BaseException | None = None

    def _target() -> None:
        nonlocal result, exception
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            try:
                result = loop.run_until_complete(coro)
            finally:
                loop.close()
        except BaseException as e:
            exception = e

    thread = threading.Thread(target=_target, daemon=True)
    thread.start()
    thread.join()

    if exception is not None:
        raise exception

    return result


@click.group(invoke_without_command=True)
@click.version_option(version=__version__, prog_name="oracle")
@click.pass_context
def main(ctx: click.Context) -> None:
    """Red Hat Oracle — AI-powered knowledge assistant."""
    ctx.ensure_object(dict)
    ctx.obj["config"] = load_config()
    if ctx.invoked_subcommand is None:
        click.echo(ctx.get_help())


@main.command()
@click.option(
    "--source",
    type=click.Choice(["github", "gitlab", "slack", "jira", "web", "pages"]),
    help="Sync only this source.",
)
@click.option(
    "--days",
    type=int,
    default=None,
    help="Override history_days for Slack/Jira (default: 730 for Slack, 365 for Jira).",
)
@click.pass_context
def ingest(ctx: click.Context, source: str | None, days: int | None) -> None:
    """Sync data sources and index into the vector store."""
    config = ctx.obj["config"]
    if days is not None:
        config.sources.slack.history_days = days
        config.sources.jira.history_days = days

    async def _ingest() -> dict[str, Any]:
        from redhat_oracle.core.sources.github import fetch_github_comments, sync_github
        from redhat_oracle.core.sources.gitlab import fetch_gitlab_comments, sync_gitlab
        from redhat_oracle.core.sources.jira import sync_jira
        from redhat_oracle.core.sources.pages import sync_pages
        from redhat_oracle.core.sources.slack import sync_slack
        from redhat_oracle.core.sources.web import sync_web
        from redhat_oracle.core.vectordb.chunker import chunk_repo
        from redhat_oracle.core.vectordb.store import VectorStore

        stats: dict[str, Any] = {}
        store = VectorStore(config)

        with Progress(console=console) as progress:
            # GitHub repos (clone/pull only — indexing below)
            if source is None or source == "github":
                task = progress.add_task("[cyan]Syncing GitHub...", total=None)
                try:
                    gh_stats = await sync_github(config, progress=progress)
                    stats["github"] = gh_stats
                except Exception as e:
                    stats["github"] = {"error": str(e)}
                progress.update(task, completed=True)

            # GitLab repos (clone/pull only — indexing below)
            if source is None or source == "gitlab":
                task = progress.add_task("[cyan]Syncing GitLab...", total=None)
                try:
                    gl_stats = await sync_gitlab(config, progress=progress)
                    stats["gitlab"] = gl_stats
                except Exception as e:
                    stats["gitlab"] = {"error": str(e)}
                progress.update(task, completed=True)

            # Code repo indexing (GitHub/GitLab)
            if source is None or source in ("github", "gitlab"):
                try:
                    from redhat_oracle.core.config import get_data_dir
                    from redhat_oracle.core.vectordb.tracker import IndexTracker

                    data_dir = get_data_dir(config)
                    repos_dir = data_dir / "repos"
                    tracker = IndexTracker(data_dir)
                    skipped_repos = 0

                    if repos_dir.exists():
                        for source_dir in repos_dir.iterdir():
                            if not source_dir.is_dir():
                                continue
                            source_name = source_dir.name
                            if source is not None and source_name != source:
                                continue
                            for org_dir in source_dir.iterdir():
                                if not org_dir.is_dir():
                                    continue
                                for repo_dir in org_dir.iterdir():
                                    if not repo_dir.is_dir():
                                        continue
                                    if not tracker.needs_reindex(
                                        source_name, org_dir.name, repo_dir.name, repo_dir
                                    ):
                                        skipped_repos += 1
                                        continue

                                    chunks = chunk_repo(
                                        repo_dir,
                                        source_name,
                                        org_dir.name,
                                        repo_dir.name,
                                    )
                                    if chunks:
                                        idx_task = progress.add_task(
                                            f"[green]Indexing {org_dir.name}/{repo_dir.name}",
                                            total=None,
                                        )
                                        store.reindex_repo(
                                            chunks,
                                            source_name,
                                            org_dir.name,
                                            repo_dir.name,
                                        )
                                        progress.update(idx_task, completed=True)

                                    tracker.mark_indexed(
                                        source_name, org_dir.name, repo_dir.name, repo_dir
                                    )

                    if skipped_repos:
                        console.print(
                            f"[dim]Skipped {skipped_repos} repos (unchanged)[/dim]"
                        )
                except Exception as e:
                    stats["code_index"] = {"error": str(e)}
                    console.print(f"[red]Code indexing error: {e}[/red]")

            # GitHub comments (upserts per-repo internally)
            if source is None or source == "github":
                try:
                    gh_comment_stats = await fetch_github_comments(
                        config, store=store, progress=progress
                    )
                    stats["github_comments"] = gh_comment_stats
                except Exception as e:
                    stats["github_comments"] = {"error": str(e)}

            # GitLab comments (upserts per-project internally)
            if source is None or source == "gitlab":
                try:
                    gl_comment_stats = await fetch_gitlab_comments(
                        config, store=store, progress=progress
                    )
                    stats["gitlab_comments"] = gl_comment_stats
                except Exception as e:
                    stats["gitlab_comments"] = {"error": str(e)}

            # Slack (upserts per-day internally)
            if source is None or source == "slack":
                try:
                    slack_stats = await sync_slack(
                        config, store=store, progress=progress
                    )
                    stats["slack"] = slack_stats
                except Exception as e:
                    stats["slack"] = {"error": str(e)}

            # Jira (upserts per-project internally)
            if source is None or source == "jira":
                try:
                    jira_stats = await sync_jira(
                        config, store=store, progress=progress
                    )
                    stats["jira"] = jira_stats
                except Exception as e:
                    stats["jira"] = {"error": str(e)}

            # Web (upserts per-site internally)
            if source is None or source == "web":
                try:
                    web_stats = await sync_web(
                        config, store=store, progress=progress
                    )
                    stats["web"] = web_stats
                except Exception as e:
                    stats["web"] = {"error": str(e)}

            # Pages (upserts per-site internally)
            if source is None or source == "pages":
                try:
                    pages_stats = await sync_pages(
                        config, store=store, progress=progress
                    )
                    stats["pages"] = pages_stats
                except Exception as e:
                    stats["pages"] = {"error": str(e)}

        stats["index"] = store.collection_stats()
        return stats

    try:
        stats = _run_async(_ingest())
        console.print("\n[bold green]Ingestion complete![/bold green]")
        for key, value in stats.items():
            console.print(f"  {key}: {value}")
    except KeyboardInterrupt:
        console.print("\n[yellow]Ingestion cancelled.[/yellow]")
        sys.exit(0)


@main.command()
@click.option("--provider", default=None, help="LLM provider override.")
@click.option("--resume", is_flag=True, help="Resume the latest session.")
@click.option("--session", "session_name", default=None, help="Resume a specific session.")
@click.pass_context
def chat(ctx: click.Context, provider: str | None, resume: bool, session_name: str | None) -> None:
    """Start an interactive chat session."""
    from prompt_toolkit import PromptSession
    from prompt_toolkit.history import InMemoryHistory

    from redhat_oracle.core.llm.base import LLMProvider
    from redhat_oracle.core.llm.external import (
        AnthropicLLM,
        OpenAILLM,
        ProviderUnavailableError,
    )
    from redhat_oracle.core.llm.ollama import OllamaLLM, OllamaUnavailableError
    from redhat_oracle.core.llm.openvino import OpenVINOLLM, OpenVINOUnavailableError
    from redhat_oracle.core.rag import RAGPipeline
    from redhat_oracle.core.session import (
        load_latest_session,
        load_session,
        new_session,
        save_session,
    )
    from redhat_oracle.core.vectordb.store import VectorStore

    config = ctx.obj["config"]

    # Load or create session
    session = None
    if session_name:
        session = load_session(session_name, config)
        if session is None:
            console.print(f"[red]Session '{session_name}' not found.[/red]")
            sys.exit(1)
        console.print(f"[green]Resumed session: {session.name}[/green]")
    elif resume:
        session = load_latest_session(config)
        if session is None:
            console.print("[yellow]No previous session found. Starting new session.[/yellow]")
        else:
            console.print(f"[green]Resumed session: {session.name}[/green]")

    if session is None:
        session = new_session(config)
        console.print(f"[green]New session: {session.name}[/green]")

    # Set up LLM
    llm_config = config.llm
    provider_name = provider or llm_config.default_provider
    llm: LLMProvider

    if provider_name == "ollama":
        llm = OllamaLLM(
            base_url=llm_config.ollama_base_url,
            model=llm_config.ollama_model,
        )
    elif provider_name == "openai":
        api_key = llm_config.openai_api_key
        if not api_key:
            console.print("[red]OpenAI API key not configured.[/red]")
            sys.exit(1)
        llm = OpenAILLM(api_key=api_key)
    elif provider_name == "anthropic":
        api_key = llm_config.anthropic_api_key
        if not api_key:
            console.print("[red]Anthropic API key not configured.[/red]")
            sys.exit(1)
        llm = AnthropicLLM(api_key=api_key)
    elif provider_name == "openvino":
        llm = OpenVINOLLM()
    else:
        console.print(f"[red]Unknown provider '{provider_name}'.[/red]")
        sys.exit(1)

    # Check LLM availability
    unavailable_errors = (
        OllamaUnavailableError,
        ProviderUnavailableError,
        OpenVINOUnavailableError,
    )
    try:
        _run_async(llm._check_available())  # type: ignore[union-attr]
    except unavailable_errors:
        while True:
            choice = click.prompt(
                f"{provider_name} is not reachable. Choose an action",
                type=click.Choice(["retry", "quit"]),
                default="retry",
            )
            if choice == "quit":
                sys.exit(0)
            try:
                _run_async(llm._check_available())  # type: ignore[union-attr]
                break
            except unavailable_errors:
                console.print("[red]Still unreachable.[/red]")

    store = VectorStore(config)
    pipeline = RAGPipeline(config, store, llm)

    prompt_session: PromptSession[str] = PromptSession(history=InMemoryHistory())
    console.print("[bold]Oracle chat ready. Type 'exit' or Ctrl+C to quit.[/bold]\n")

    try:
        while True:
            try:
                user_input = prompt_session.prompt("You> ")
            except EOFError:
                break

            if not user_input.strip():
                continue
            if user_input.strip().lower() in ("exit", "quit"):
                break

            session.add_message("user", user_input)

            async def _stream(question: str = user_input) -> str:
                token_stream, sources = await pipeline.stream_query(
                    question, session=session,
                )
                full_response: list[str] = []
                console.print("[bold cyan]Oracle>[/bold cyan] ", end="")
                async for token in token_stream:
                    console.print(token, end="", highlight=False)
                    full_response.append(token)
                console.print()  # newline after response

                if sources:
                    console.print("\n[dim]Sources:[/dim]")
                    for s in sources:
                        url = s.get('url', '')
                        if url:
                            ref = url
                        else:
                            ref = "/".join(
                                v for v in (s.get('source', '?'), s.get('org', ''),
                                            s.get('repo', ''), s.get('path', '?'))
                                if v
                            )
                        console.print(
                            f"  [dim]{ref} (score: {s['score']:.2f})[/dim]"
                        )
                    console.print()

                return "".join(full_response)

            try:
                response_text = _run_async(_stream())
                source_refs = [{"source": "oracle"}]
                session.add_message("assistant", response_text, sources=source_refs)
                save_session(session, config)
            except unavailable_errors:
                console.print(f"[red]{provider_name} became unreachable.[/red]")
                break

    except KeyboardInterrupt:
        console.print("\n[yellow]Chat ended.[/yellow]")

    save_session(session, config)
    console.print(f"[green]Session saved: {session.name}[/green]")


@main.command()
@click.argument("query")
@click.option(
    "--source",
    type=click.Choice(["github", "gitlab", "slack", "jira", "web", "pages", "code"]),
    help="Filter by source.",
)
@click.option("--top-k", default=10, help="Number of results.")
@click.pass_context
def search(ctx: click.Context, query: str, source: str | None, top_k: int) -> None:
    """Search the vector store without using the LLM."""
    from redhat_oracle.core.vectordb.store import VectorStore

    config = ctx.obj["config"]
    store = VectorStore(config)

    results = store.search(query, source=source, top_k=top_k)

    if not results:
        console.print("[yellow]No results found.[/yellow]")
        return

    for i, result in enumerate(results, 1):
        meta = result.get("metadata", {})
        score = result.get("score", 0.0)
        url = meta.get("url", "")
        if url:
            path = url
        else:
            path = "/".join(
                v for v in (meta.get('source', '?'), meta.get('org', ''),
                            meta.get('repo', ''), meta.get('path', '?'))
                if v
            )
        text_preview = result.get("text", "")[:200]

        table = Table(title=f"Result {i}", show_header=False, border_style="dim")
        table.add_row("Path", path)
        table.add_row("Score", f"{score:.4f}")
        table.add_row("Preview", text_preview)
        console.print(table)
        console.print()


@main.command()
@click.pass_context
def status(ctx: click.Context) -> None:
    """Show collection stats and system status."""
    from redhat_oracle.core.vectordb.store import VectorStore

    config = ctx.obj["config"]
    store = VectorStore(config)

    stats = store.collection_stats()

    table = Table(title="Vector Store Status")
    table.add_column("Collection", style="cyan")
    table.add_column("Chunks", style="green", justify="right")

    total = 0
    for name, count in stats.items():
        table.add_row(name, str(count))
        total += count

    table.add_row("[bold]Total[/bold]", f"[bold]{total}[/bold]")
    console.print(table)

    console.print(f"\n[dim]LLM provider: {config.llm.default_provider}[/dim]")
    console.print(f"[dim]Embedding model: {config.vectordb.embeddings.model}[/dim]")


@main.command()
@click.pass_context
def gui(ctx: click.Context) -> None:
    """Launch the desktop GUI."""
    from redhat_oracle.gui.app import launch_gui

    sys.exit(launch_gui(ctx.obj["config"]))


@main.command("config")
@click.option("--edit", is_flag=True, help="Open config in $EDITOR.")
@click.pass_context
def config_cmd(ctx: click.Context, edit: bool) -> None:
    """Show or edit the current configuration."""
    import yaml

    config = ctx.obj["config"]

    if edit:
        from redhat_oracle.core.config import _config_paths

        paths = _config_paths()
        # Find the first existing config or use the project-level one
        config_path = None
        for p in reversed(paths):
            if p.is_file():
                config_path = p
                break
        if config_path is None:
            config_path = paths[-1]  # project config
            config_path.parent.mkdir(parents=True, exist_ok=True)
            if not config_path.exists():
                config_path.write_text("# Red Hat Oracle config\n")

        import os

        editor = os.environ.get("EDITOR", "vi")
        subprocess.run([editor, str(config_path)], check=False)
        return

    config_dict = config.model_dump()
    # Mask secrets
    for key_path in [
        ("sources", "github", "token"),
        ("sources", "gitlab", "token"),
        ("sources", "slack", "token"),
        ("sources", "jira", "token"),
        ("llm", "openai_api_key"),
        ("llm", "anthropic_api_key"),
    ]:
        obj = config_dict
        for k in key_path[:-1]:
            obj = obj.get(k, {})
        last = key_path[-1]
        if obj.get(last):
            obj[last] = "***"

    console.print(yaml.dump(config_dict, default_flow_style=False))
