# src/chitai/cli.py

"""
Extra commands on the `litestar` CLI.

Registered through `CalibreCLIPlugin` in `app.py`, so they run as
`litestar --app-dir src/chitai/ calibre-import …` and get the app's own configuration
without a second way to load it.

The Calibre import lives here as well as behind an endpoint because the case it exists
for is a one-time migration of a library that may be hundreds of gigabytes. That should
not depend on a browser tab staying open.
"""

from __future__ import annotations

import asyncio
from pathlib import Path

import click
from click import Group
from litestar.plugins import CLIPluginProtocol

from chitai.config import settings
from chitai.database.models import Library
from chitai.services.book import BookService, CalibreImportProgress, CalibreImportResult
from chitai.services.calibre import CalibreLibrary, CalibreLibraryError
from chitai.services.library import LibraryService


class CalibreCLIPlugin(CLIPluginProtocol):
    """Adds `calibre-import` to the Litestar CLI."""

    def on_cli_init(self, cli: Group) -> None:
        cli.add_command(calibre_import)


@click.command(name="calibre-import")
@click.argument(
    "source",
    type=click.Path(exists=True, file_okay=False, path_type=Path),
)
@click.option(
    "--library",
    "library_slug",
    required=True,
    help="Slug of the Chitai library to import into.",
)
@click.option(
    "--allow-duplicates",
    is_flag=True,
    help="Import books whose files the library already holds.",
)
@click.option(
    "--dry-run",
    is_flag=True,
    help="Read the catalogue and report what it holds, without writing anything.",
)
def calibre_import(
    source: Path, library_slug: str, allow_duplicates: bool, dry_run: bool
) -> None:
    """
    Import a Calibre library from SOURCE, the directory holding its metadata.db.

    Files are copied, never moved: the Calibre library is left exactly as it is, and
    re-running skips whatever is already stored.
    """
    asyncio.run(_import(source, library_slug, allow_duplicates, dry_run))


async def _import(
    source: Path, library_slug: str, allow_duplicates: bool, dry_run: bool
) -> None:
    try:
        library_source = CalibreLibrary(source)
        await library_source.open()
    except CalibreLibraryError as exc:
        raise click.ClickException(str(exc)) from exc

    try:
        if dry_run:
            await _report(library_source)
            return

        async with settings.alchemy_config.get_session() as session:
            library = await _library(session, library_slug)

            result = await BookService(session=session).create_many_from_calibre(
                library_source,
                library,
                allow_duplicates=allow_duplicates,
                on_progress=_print_progress,
            )
    finally:
        await library_source.close()

    _print_summary(result)

    if result.failed:
        raise SystemExit(1)


async def _library(session: object, slug: str) -> Library:
    """Resolve the target library, or explain what the options were."""
    service = LibraryService(session=session)  # type: ignore[arg-type]

    library = await service.get_one_or_none(Library.slug == slug)

    if library is None:
        available = ", ".join(sorted(item.slug for item in await service.list()))
        raise click.ClickException(
            f"No library with slug '{slug}'. Available: {available or 'none'}"
        )

    # A read-only library is one pointing at a tree Chitai does not own. Copying books
    # into it would write into somebody else's directory.
    if library.read_only:
        raise click.ClickException(
            f"Library '{slug}' is read-only, so nothing can be imported into it"
        )

    return library


async def _report(source: CalibreLibrary) -> None:
    """Describe the catalogue without touching the database."""
    books = await source.books()

    click.echo(f"{len(books)} book(s) in {source.root}\n")

    for book in books:
        authors = ", ".join(book.authors) or "unknown author"
        formats = ", ".join(file.format for file in book.files) or "no files"
        click.echo(f"  #{book.calibre_id:<6} {book.title}")
        click.echo(f"  {'':<7} {authors} · {formats}")

    missing = [
        book
        for book in books
        if any(not file.path.is_file() for file in book.files) or not book.files
    ]

    if missing:
        click.echo(
            f"\n{len(missing)} book(s) have files the catalogue lists "
            "but disk does not:"
        )
        for book in missing:
            click.echo(f"  #{book.calibre_id} {book.title}")


def _print_progress(progress: CalibreImportProgress) -> None:
    marker = {"created": "+", "skipped": "-", "failed": "!"}.get(progress.outcome, " ")
    detail = f" ({progress.detail})" if progress.detail else ""

    click.echo(
        f"[{progress.processed:>5}/{progress.total}] {marker} {progress.title}{detail}"
    )


def _print_summary(result: CalibreImportResult) -> None:
    click.echo(
        f"\n{len(result.created)} created, {len(result.skipped)} skipped, "
        f"{len(result.failed)} failed, of {result.total}."
    )

    if result.duplicate_files:
        click.echo(
            f"{len(result.duplicate_files)} individual file(s) were already stored and "
            "were left out of books that imported otherwise."
        )

    for failure in result.failed:
        click.echo(f"  failed  #{failure.calibre_id} {failure.title}: {failure.reason}")

    # Imported all the same — a metadata match is a guess, and the duplicates screen is
    # where these get decided.
    for possible in result.possible_duplicates:
        names = ", ".join(
            f"{candidate.title} (#{candidate.book_id})"
            for candidate in possible.candidates
        )
        click.echo(
            f"  possible duplicate  {possible.title} (#{possible.book_id}) "
            f"may already be in the library as: {names}"
        )
