"""Command-line client for llmconnector.

Reads the API key from an environment variable derived from the provider name
("{PROVIDER}_API_KEY", e.g. OPENAI_API_KEY) and exposes
simple commands to generate a response or list available models for a
provider.
"""

from __future__ import annotations

import argparse
import json
import os
import sys
from typing import Sequence

# Support both package execution and direct file execution.
# When run as a script (no package), add the parent of this file's directory to sys.path
# so that `import llmconnector` resolves.
try:
    if __package__ in (None, ""):
        # Running as a script: add repo/src to path
        _here = os.path.dirname(os.path.abspath(__file__))
        _pkg_root = os.path.dirname(_here)
        if _pkg_root not in sys.path:
            sys.path.insert(0, _pkg_root)
        from llmconnector.client import LLMClient  # type: ignore
    else:
        from .client import LLMClient
except Exception:  # pragma: no cover - defensive import fallback
    # Last-resort fallback if the above logic fails in unusual environments
    from llmconnector.client import LLMClient  # type: ignore


def _env_api_key(provider: str) -> str:
    env_name = f"{provider.upper()}_API_KEY"
    api_key = os.environ.get(env_name, "")
    if not api_key:
        raise SystemExit(
            f"Missing {env_name} environment variable. Set it before running."
        )
    return api_key


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="client_cli",
        description="CLI for provider-agnostic LLM requests",
    )

    subparsers = parser.add_subparsers(dest="command", required=True)

    # respond: generate a model response
    p_respond = subparsers.add_parser(
        "respond", help="Generate a response from a provider model"
    )
    p_respond.add_argument(
        "--provider",
        required=False,
        help="Provider to use (e.g. openai, gemini, anthropic, grok). If omitted, you will be prompted.",
    )
    p_respond.add_argument(
        "--model",
        required=False,
        help="Model identifier for the provider (e.g. gpt-4o). If omitted, you will be prompted.",
    )
    p_respond.add_argument(
        "--prompt",
        default="",
        help="Text prompt to send (omit if only using images)",
    )
    p_respond.add_argument(
        "--image",
        action="append",
        dest="images",
        default=None,
        help="Image path or URL; may be provided multiple times",
    )
    p_respond.add_argument(
        "--max-tokens",
        type=int,
        default=32000,
        help="Maximum output tokens (provider-specific meaning)",
    )
    p_respond.add_argument(
        "--reasoning-effort",
        choices=["low", "medium", "high"],
        default=None,
        help="Optional reasoning effort hint if supported",
    )

    # models: list available models
    p_models = subparsers.add_parser(
        "models", help="List models available to the provider"
    )
    p_models.add_argument(
        "--provider",
        required=False,
        help="Provider to query (e.g. openai, gemini, anthropic, grok). If omitted, you will be prompted.",
    )
    p_models.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON (default is human-readable)",
    )

    # all-models: list models for all registered providers
    p_all_models = subparsers.add_parser(
        "all-models", help="List models for all registered providers"
    )
    p_all_models.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON (default is human-readable)",
    )

    return parser


def _cmd_respond(args: argparse.Namespace) -> int:
    client = LLMClient()
    provider = args.provider
    if not provider:
        # Try to hint known providers
        try:
            known = sorted(LLMClient._discover_default_providers().keys())  # type: ignore[attr-defined]
        except Exception:
            known = []
        hint = f" ({'/'.join(known)})" if known else ""
        provider = input(f"Provider{hint}: ").strip()
        if not provider:
            print("Error: provider is required.", file=sys.stderr)
            return 2

    api_key = _env_api_key(provider)

    model = args.model
    if not model:
        model = input("Model id: ").strip()
        if not model:
            print("Error: model is required.", file=sys.stderr)
            return 2

    prompt = args.prompt
    images: Sequence[str] | None = args.images
    if not prompt and not images:
        prompt = input("Prompt: ")
        if not prompt and not images:
            print("Error: provide a prompt or at least one image.", file=sys.stderr)
            return 2
    try:
        output = client.generate_response(
            provider=provider,
            api_key=api_key,
            prompt=prompt,
            model=model,
            max_tokens=args.max_tokens,
            reasoning_effort=args.reasoning_effort,
            images=images,
        )
    except Exception as exc:  # pragma: no cover - CLI surface
        print(f"Error: {exc}", file=sys.stderr)
        return 2

    print(output)
    return 0


def _cmd_models(args: argparse.Namespace) -> int:
    client = LLMClient()
    provider = args.provider
    if not provider:
        try:
            known = sorted(LLMClient._discover_default_providers().keys())  # type: ignore[attr-defined]
        except Exception:
            known = []
        hint = f" ({'/'.join(known)})" if known else ""
        provider = input(f"Provider{hint}: ").strip()
        if not provider:
            print("Error: provider is required.", file=sys.stderr)
            return 2

    api_key = _env_api_key(provider)
    try:
        models = client.list_models(provider=provider, api_key=api_key)
    except Exception as exc:  # pragma: no cover - CLI surface
        print(f"Error: {exc}", file=sys.stderr)
        return 2

    if args.json:
        print(json.dumps(models, indent=2))
    else:
        if not models:
            print("No models found.")
        else:
            for m in models:
                mid = m.get("id") or "<unknown>"
                name = m.get("display_name") or ""
                if name:
                    print(f"{mid} - {name}")
                else:
                    print(mid)
    return 0


def _env_api_key_with_fallbacks(provider: str) -> tuple[str | None, list[str]]:
    """Return the API key for a provider, trying common env var fallbacks.

    Returns a tuple of (api_key_or_none, tried_env_names).
    """
    name = provider.upper()
    # Default convention first
    env_names: list[str] = [f"{name}_API_KEY"]

    # Provider-specific fallbacks commonly used by SDKs/docs
    if provider.lower() == "gemini":
        env_names.extend(["GOOGLE_API_KEY"])  # google-genai default
    elif provider.lower() in {"grok", "xai"}:
        env_names.extend(["XAI_API_KEY", "GROK_API_KEY"])  # prefer XAI
    elif provider.lower() == "openai":
        # OPENAI_API_KEY already covered by default
        pass
    elif provider.lower() == "anthropic":
        # ANTHROPIC_API_KEY already covered by default
        pass

    for env_name in env_names:
        val = os.environ.get(env_name)
        if val:
            return val, env_names

    return None, env_names


def _cmd_all_models(args: argparse.Namespace) -> int:
    client = LLMClient()

    # Group provider names by underlying client instance to avoid duplicates
    by_client: dict[int, dict[str, object]] = {}
    for name, prov_client in getattr(client, "_providers", {}).items():  # type: ignore[attr-defined]
        key = id(prov_client)
        group = by_client.setdefault(key, {"names": [], "client": prov_client})
        names = group["names"]  # type: ignore[assignment]
        assert isinstance(names, list)
        names.append(name)

    results: list[dict[str, object]] = []
    for entry in by_client.values():
        names = sorted(entry["names"])  # type: ignore[index]
        primary = names[0]
        display_name = "/".join(names)

        api_key, tried = _env_api_key_with_fallbacks(primary)
        if not api_key:
            results.append(
                {
                    "provider": display_name,
                    "error": f"missing API key (tried: {', '.join(tried)})",
                    "models": [],
                }
            )
            continue

        try:
            models = client.list_models(provider=primary, api_key=api_key)
        except Exception as exc:  # pragma: no cover - CLI surface
            results.append(
                {
                    "provider": display_name,
                    "error": str(exc),
                    "models": [],
                }
            )
            continue

        results.append({"provider": display_name, "models": models})

    if args.json:
        print(json.dumps(results, indent=2))
    else:
        if not results:
            print("No providers registered.")
            return 0

        for item in results:
            provider_label = str(item.get("provider", "<unknown>"))
            print(f"== {provider_label} ==")
            if item.get("error"):
                print(f"  Skipped: {item['error']}")
                continue
            models = item.get("models") or []
            if not models:
                print("  No models found.")
                continue
            for m in models:  # type: ignore[assignment]
                if not isinstance(m, dict):
                    print(f"  {m}")
                    continue
                mid = m.get("id") or "<unknown>"
                name = m.get("display_name") or ""
                if name:
                    print(f"  {mid} - {name}")
                else:
                    print(f"  {mid}")

    return 0


def main(argv: Sequence[str] | None = None) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)

    if args.command == "respond":
        return _cmd_respond(args)
    if args.command == "models":
        return _cmd_models(args)
    if args.command == "all-models":
        return _cmd_all_models(args)

    parser.error("unknown command")
    return 2


if __name__ == "__main__":
    raise SystemExit(main())
