# retoor <retoor@molodetz.nl>

from __future__ import annotations

import argparse
import asyncio
import logging
import os
from dataclasses import replace
from pathlib import Path
from typing import Any, Optional

import dataset

from . import console
from .actions import Dispatcher
from .agent import Agent
from .agentic import AgenticController, LessonStore
from .chunks import ChunkStore
from .config import Settings, load_settings
from .cost import CostTracker
from .errors import AuthRequiredError
from .http_client import PlatformClient
from .llm import LLMClient
from .registry import CATALOG
from .tasks import Scheduler, TaskController, TaskStore

logger = logging.getLogger("devii.cli")

EXIT_COMMANDS = {"/exit", "/quit", "/q"}
WELCOME = "Devii - DevPlace assistant"
LOGIN_REQUEST = (
    "Welcome. I manage your DevPlace account, but you must log in first. "
    "Please tell me your email and password to continue."
)
GOODBYE = "Session ended."
RESULT_NOTICE_CHARS = 20000


def _cli_tasks_db_path() -> Path:
    base = os.environ.get("DEVII_HOME") or os.environ.get("XDG_DATA_HOME")
    root = Path(base) if base else Path.home() / ".local" / "share"
    target = root / "devii"
    target.mkdir(parents=True, exist_ok=True)
    return target / "devii_tasks.db"


def _label(row: dict[str, Any]) -> str:
    return row.get("label") or row.get("uid", "task")


def _make_event_handler() -> Any:
    def handle(kind: str, row: dict[str, Any], payload: str) -> None:
        name = _label(row)
        if kind == "start":
            console.notice(f"\n[task {name}] running...")
        elif kind in ("done", "finished"):
            suffix = " (completed, no further runs)" if kind == "finished" else ""
            console.notice(f"[task {name}] finished{suffix}:")
            console.assistant(payload[:RESULT_NOTICE_CHARS])
        elif kind == "error":
            console.warn(f"[task {name}] failed: {payload}")

    return handle


TRACE_GLYPHS = {
    "call": "↳",
    "ok": "✓",
    "err": "✗",
    "plan-gate": "plan required first",
    "reflect-trigger": "reflecting on error",
    "verify-gate": "verification gate",
    "compact": "context compaction",
}


def _trace(event: str, name: str = "", detail: str = "") -> None:
    glyph = TRACE_GLYPHS.get(event, event)
    if event in ("call", "ok", "err"):
        suffix = f" {detail}" if detail else ""
        console.notice(f"  {glyph} {name}{suffix}")
    else:
        console.notice(f"  ! {glyph}")


def _make_executor(
    settings: Settings,
    llm: LLMClient,
    dispatcher: Dispatcher,
    lessons: LessonStore,
    cost_tracker: CostTracker,
    chunk_store: ChunkStore,
) -> Any:
    tools = CATALOG.tool_schemas()

    async def execute(prompt: str) -> str:
        worker = Agent(
            settings,
            llm,
            dispatcher,
            tools,
            lessons=lessons,
            cost_tracker=cost_tracker,
            chunk_store=chunk_store,
        )
        return await worker.respond(prompt)

    return execute


async def _bootstrap_auth(client: PlatformClient, settings: Settings) -> str:
    if client.authenticated:
        logger.info("Authenticated via API key")
        return "Authenticated via API key. How can I help?"
    if settings.login_email and settings.login_password:
        try:
            await client.login(settings.login_email, settings.login_password)
            return f"Logged in as {client.username}. How can I help?"
        except AuthRequiredError as exc:
            console.warn(f"[auth] automatic login failed: {exc.message}")
    return LOGIN_REQUEST


def _resolve_owner(settings: Settings) -> tuple[str, str]:
    try:
        from devplacepy.database import db, get_table

        if "users" not in db.tables:
            return "user", "cli"
        users = get_table("users")
        user = None
        if settings.platform_api_key:
            user = users.find_one(api_key=settings.platform_api_key)
        if user is None and settings.login_email:
            user = users.find_one(email=settings.login_email.lower())
        if user:
            return "user", user["uid"]
    except Exception:  # noqa: BLE001 - no local DB / remote-only: fall back to a standalone store
        return "user", "cli"
    return "user", "cli"


def _build_stores(owner_kind: str, owner_id: str):
    if owner_id == "cli":
        owned_db = dataset.connect(f"sqlite:///{_cli_tasks_db_path()}")
    else:
        from devplacepy.database import (
            db as owned_db,
        )  # share the platform DB for this account
    return TaskStore(owned_db, owner_kind, owner_id, operator=True), LessonStore(
        owned_db, owner_kind, owner_id
    )


async def run(settings: Settings, prompt: Optional[str] = None) -> None:
    client = PlatformClient(
        settings.base_url, settings.timeout_seconds, settings.platform_api_key
    )
    llm = LLMClient(settings)
    greeting = await _bootstrap_auth(client, settings)

    owner_kind, owner_id = _resolve_owner(settings)
    store, lessons = _build_stores(owner_kind, owner_id)
    controller = TaskController(store)
    cost_tracker = CostTracker()
    chunk_store = ChunkStore()
    agentic = AgenticController(lessons, settings)
    dispatcher = Dispatcher(
        CATALOG,
        client,
        settings,
        controller,
        agentic,
        is_admin=True,
        is_primary_admin=True,
    )
    tools = CATALOG.tool_schemas_for(
        client.authenticated, is_admin=True, is_primary_admin=True
    )
    agentic.bind(
        llm=llm,
        dispatcher=dispatcher,
        tools=tools,
        on_trace=_trace,
        cost_tracker=cost_tracker,
        chunk_store=chunk_store,
    )
    agent = Agent(
        settings,
        llm,
        dispatcher,
        tools,
        lessons=lessons,
        on_trace=_trace,
        cost_tracker=cost_tracker,
        chunk_store=chunk_store,
    )

    if prompt is not None:
        try:
            reply = await agent.respond(prompt)
            print(reply)
        finally:
            await client.aclose()
            await llm.aclose()
        return

    scheduler = Scheduler(
        store,
        _make_executor(settings, llm, dispatcher, lessons, cost_tracker, chunk_store),
        _make_event_handler(),
        settings.scheduler_tick_seconds,
    )

    console.banner(WELCOME)
    console.notice(f"Endpoint {settings.base_url} | model {settings.ai_model}")
    console.notice(
        "Type /exit to quit. Agentic ReAct loop with planning, self-learning memory, and "
        "background tasks is active.\n"
    )
    console.assistant(greeting)

    scheduler.start()
    try:
        while True:
            try:
                user_text = (await console.prompt("you> ")).strip()
            except (EOFError, KeyboardInterrupt):
                print()
                break

            if not user_text:
                continue
            if user_text.lower() in EXIT_COMMANDS:
                break

            try:
                reply = await agent.respond(user_text)
            except Exception as exc:  # noqa: BLE001 - keep the chat alive
                logger.exception("Unhandled error while responding")
                console.warn(f"[error] {exc}")
                continue

            console.assistant(reply)
    finally:
        await scheduler.stop()
        await client.aclose()
        await llm.aclose()
        console.notice(GOODBYE)


def _parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        prog="devii",
        description="Agentic chat assistant that manages a DevPlace account.",
    )
    parser.add_argument(
        "-p", "--prompt", help="Run a single prompt non-interactively and exit."
    )
    parser.add_argument(
        "--api-key", help="DevPlace API key for automatic authentication (Bearer)."
    )
    parser.add_argument("--email", help="Login email for automatic basic-auth login.")
    parser.add_argument(
        "--password", help="Login password for automatic basic-auth login."
    )
    parser.add_argument(
        "--basic-auth", help="Shorthand credentials in the form email:password."
    )
    parser.add_argument("--base-url", help="Override the platform base URL.")
    parser.add_argument("--ai-url", help="Override the model endpoint URL.")
    parser.add_argument("--ai-key", help="Override the model API key.")
    parser.add_argument("--model", help="Override the model name.")
    return parser.parse_args(argv)


def _settings_from_args(args: argparse.Namespace) -> Settings:
    settings = load_settings()
    email = args.email or settings.login_email
    password = args.password or settings.login_password
    if args.basic_auth and ":" in args.basic_auth:
        email, password = args.basic_auth.split(":", 1)
    overrides: dict[str, Any] = {"login_email": email, "login_password": password}
    if args.api_key:
        overrides["platform_api_key"] = args.api_key
    if args.base_url:
        overrides["base_url"] = args.base_url.rstrip("/")
    if args.ai_url:
        overrides["ai_url"] = args.ai_url
    if args.ai_key:
        overrides["ai_key"] = args.ai_key
    if args.model:
        overrides["ai_model"] = args.model
    return replace(settings, **overrides)


NOISY_LOGGERS = (
    "httpcore",
    "httpx",
    "httpcore.http11",
    "httpcore.connection",
    "asyncio",
    "urllib3",
)


def main(argv: Optional[list[str]] = None) -> None:
    args = _parse_args(argv)
    settings = _settings_from_args(args)
    logging.basicConfig(
        level=getattr(logging, settings.log_level, logging.WARNING),
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    )
    for name in NOISY_LOGGERS:
        logging.getLogger(name).setLevel(logging.WARNING)
    logger.info("Starting Devii")
    try:
        asyncio.run(run(settings, prompt=args.prompt))
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
