"""Main application entry point for AI MusiMuse.

This module defines a Typer CLI application with:

- A default callback that bootstraps the project (config, logging, DB, banner).
- A ``scan`` command that runs the file scanner and track importer.
- A ``decode`` command that decodes tracks and stores metadata.
- An ``analyze`` command that runs the analysis pipeline on all tracks.
- A ``dna`` command that builds MusicDNA from stored features.
- An ``encode`` command that encodes MusicDNA into float32 vectors.
- A ``compare`` command that compares two tracks by similarity.

No business logic is implemented here.  The CLI only orchestrates
application services.
"""

from __future__ import annotations

from pathlib import Path

import typer
from rich.console import Console
from rich.panel import Panel
from rich.text import Text

from analyzer.context import AnalysisContext
from analyzer.default_registry import build_default_registry
from analyzer.pipeline import AnalysisPipeline
from config.settings import Settings, get_settings
from core.exceptions import DecodingError
from core.version import __version__
from database.database import get_session_factory, init_db
from database.models import Track, TrackStatus
from database.repositories import DecoderRepository, ProjectRepository, TrackRepository
from decoder.decoder import AudioDecoder
from features.default_registry import build_default_feature_registry
from importer.importer import TrackImporter
from log.logger import configure_logging, get_logger
from music_dna.builder import MusicDNABuilder
from music_dna.encoder import MusicDNAEncoder
from music_dna.exceptions import MusicDNAError
from scanner.scanner import FileScanner
from similarity.engine import SimilarityEngine
from similarity.exceptions import SimilarityError
from storage.feature_repository import FeatureRepository

app = typer.Typer(
    name="musimuse",
    help=(
        "AI MusiMuse — offline-first research platform"
        " for understanding musical identity."
    ),
    no_args_is_help=False,
    add_completion=False,
)

console = Console()


def _bootstrap() -> tuple[Settings, object]:
    """Load settings, configure logging, and initialize the database.

    Returns:
        A tuple of ``(settings, logger)`` for use by CLI commands.
    """
    settings = get_settings()
    settings.ensure_directories()

    configure_logging(level=settings.log_level)
    logger = get_logger(__name__)

    init_db(database_url=settings.database_url)

    return settings, logger


def _print_banner(settings: Settings) -> None:
    """Print the AI MusiMuse startup banner using Rich.

    Args:
        settings: The loaded runtime settings.
    """
    banner_text = Text()
    banner_text.append("AI MusiMuse\n", style="bold cyan")
    banner_text.append(f"Version: {__version__}\n", style="dim")
    banner_text.append(f"Project: {settings.project_name}\n", style="green")
    banner_text.append(f"Database: {settings.database_url}\n", style="blue")
    banner_text.append(f"Log Level: {settings.log_level}", style="yellow")

    panel = Panel(
        banner_text,
        title="[bold magenta]AI MusiMuse[/bold magenta]",
        border_style="cyan",
        padding=(1, 2),
    )
    console.print(panel)


@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
) -> None:
    """Bootstrap AI MusiMuse.

    Loads configuration, initializes logging and the database, prints
    a startup banner, and exits.  No business logic is executed.
    """
    settings, logger = _bootstrap()

    logger.info("startup.begin", project=settings.project_name, version=__version__)
    logger.info("database.initialized", url=settings.database_url)

    _print_banner(settings)

    logger.info("startup.complete")


@app.command()
def scan() -> None:
    """Scan the music directory and import tracks into the database."""
    settings, logger = _bootstrap()

    music_dir = Path(settings.data_directory) / "music"
    logger.info("scan.command.started", directory=str(music_dir))

    scanner = FileScanner(music_directory=music_dir)
    collection = scanner.scan()

    session_factory = get_session_factory()
    session = session_factory()
    try:
        result = TrackImporter.import_manifests(session, collection)
    finally:
        session.close()

    console.print()
    console.print("[bold cyan]Scan Summary[/bold cyan]")
    console.print(f"  New tracks:   {result.new_tracks}")
    console.print(f"  Duplicates:   {result.duplicates}")
    console.print(f"  Modified:     {result.modified}")
    console.print(f"  Deleted:      {result.deleted}")
    console.print(f"  Unchanged:    {result.unchanged}")
    console.print()

    logger.info(
        "scan.command.complete",
        new=result.new_tracks,
        duplicates=result.duplicates,
        modified=result.modified,
        deleted=result.deleted,
        unchanged=result.unchanged,
    )


@app.command()
def decode() -> None:
    """Decode tracks without metadata and store extracted metadata."""
    settings, logger = _bootstrap()

    music_dir = Path(settings.data_directory) / "music"
    logger.info("decode.command.started", directory=str(music_dir))

    session_factory = get_session_factory()
    session = session_factory()
    decoded_count = 0
    failed_count = 0
    try:
        project = ProjectRepository.get_default_project(session)
        tracks = DecoderRepository.list_tracks_without_metadata(session, project.id)
        logger.info("decode.tracks_found", count=len(tracks))
        logger.info("decoder.started", count=len(tracks))

        decoder = AudioDecoder()
        for track in tracks:
            file_path = music_dir / track.relative_path
            try:
                decoded = decoder.decode(file_path)
                DecoderRepository.update_metadata(
                    session,
                    track,
                    duration=decoded.duration,
                    sample_rate=decoded.sample_rate,
                    channels=decoded.channels,
                    bit_depth=decoded.bit_depth,
                )
                decoded_count += 1
                logger.info(
                    "metadata.stored",
                    path=track.relative_path,
                )
            except DecodingError as exc:
                failed_count += 1
                logger.warning(
                    "decoder.failed",
                    path=track.relative_path,
                    error=str(exc),
                )
                continue

        session.commit()
        logger.info(
            "decoder.finished",
            decoded=decoded_count,
            failed=failed_count,
        )
    finally:
        session.close()

    console.print()
    console.print("[bold cyan]Decode Summary[/bold cyan]")
    console.print(f"  Decoded:  {decoded_count}")
    console.print(f"  Failed:   {failed_count}")
    console.print()

    logger.info(
        "decode.command.complete",
        decoded=decoded_count,
        failed=failed_count,
    )


@app.command()
def analyze() -> None:
    """Analyze all active tracks using the analysis pipeline."""
    settings, logger = _bootstrap()

    music_dir = Path(settings.data_directory) / "music"
    logger.info("analysis.started", directory=str(music_dir))

    session_factory = get_session_factory()
    session = session_factory()
    analyzed_count = 0
    failed_count = 0
    analyzer_runs = 0
    features_stored = 0
    try:
        project = ProjectRepository.get_default_project(session)
        all_tracks = TrackRepository.list_tracks(session, project.id)
        active_tracks = [t for t in all_tracks if t.status == TrackStatus.ACTIVE]
        logger.info(
            "analysis.tracks_found",
            total=len(all_tracks),
            active=len(active_tracks),
        )

        decoder = AudioDecoder()

        registry = build_default_registry()
        pipeline = AnalysisPipeline(registry)

        for track in active_tracks:
            file_path = music_dir / track.relative_path
            try:
                decoded = decoder.decode(file_path)
            except DecodingError as exc:
                failed_count += 1
                logger.warning(
                    "analysis.decode_failed",
                    path=track.relative_path,
                    error=str(exc),
                )
                continue

            context = AnalysisContext(
                track=track,
                decoded_audio=decoded,
                settings=settings,
            )
            results = pipeline.run(context, session=session)
            analyzed_count += 1

            for result in results:
                if result.success and len(result.feature_set) > 0:
                    analyzer_runs += 1
                    features_stored += len(result.feature_set)

        session.commit()
    finally:
        session.close()

    console.print()
    console.print("[bold cyan]Analysis Summary[/bold cyan]")
    console.print(f"  Tracks analyzed:    {analyzed_count}")
    console.print(f"  Analyzer runs:      {analyzer_runs}")
    console.print(f"  Features stored:    {features_stored}")
    console.print(f"  Failures:           {failed_count}")
    console.print()

    logger.info(
        "analysis.finished",
        analyzed=analyzed_count,
        analyzer_runs=analyzer_runs,
        features_stored=features_stored,
        failures=failed_count,
    )


@app.command()
def dna() -> None:
    """Build MusicDNA from stored features for all active tracks."""
    import datetime as _dt

    settings, logger = _bootstrap()

    logger.info("dna.started")

    feature_registry = build_default_feature_registry()
    builder = MusicDNABuilder(feature_registry)

    session_factory = get_session_factory()
    session = session_factory()
    processed = 0
    successful = 0
    failed = 0
    total_features = 0
    try:
        project = ProjectRepository.get_default_project(session)
        all_tracks = TrackRepository.list_tracks(session, project.id)
        active_tracks = [t for t in all_tracks if t.status == TrackStatus.ACTIVE]

        created_at = _dt.datetime.now(_dt.UTC).isoformat()

        for track in active_tracks:
            processed += 1
            logger.info(
                "dna.track",
                track_id=track.id,
                path=track.relative_path,
            )

            features = FeatureRepository.get_latest_features(session, track.id)
            if not features:
                failed += 1
                logger.warning(
                    "dna.validation_failed",
                    track_id=track.id,
                    error="No features found",
                )
                continue

            try:
                music_dna = builder.build(
                    track_id=track.id,
                    features=features,
                    created_at=created_at,
                )
                successful += 1
                total_features += len(music_dna.values)
            except MusicDNAError as exc:
                failed += 1
                logger.warning(
                    "dna.validation_failed",
                    track_id=track.id,
                    error=str(exc),
                )
                continue
    finally:
        session.close()

    avg_features = total_features / successful if successful > 0 else 0

    console.print()
    console.print("[bold cyan]Music DNA Summary[/bold cyan]")
    console.print(f"  Tracks processed:  {processed}")
    console.print(f"  Successful:        {successful}")
    console.print(f"  Failed:            {failed}")
    console.print(f"  Schema version:    {MusicDNABuilder.SCHEMA_VERSION}")
    console.print(f"  Average features:  {avg_features:.0f}")
    console.print()

    logger.info(
        "dna.completed",
        processed=processed,
        successful=successful,
        failed=failed,
        average_features=round(avg_features, 1),
    )


@app.command()
def encode() -> None:
    """Encode MusicDNA into fixed-size float32 vectors for all tracks."""
    import datetime as _dt

    settings, logger = _bootstrap()

    logger.info("encoder.started")

    feature_registry = build_default_feature_registry()
    builder = MusicDNABuilder(feature_registry)
    encoder = MusicDNAEncoder()

    session_factory = get_session_factory()
    session = session_factory()
    processed = 0
    encoded_count = 0
    failed = 0
    try:
        project = ProjectRepository.get_default_project(session)
        all_tracks = TrackRepository.list_tracks(session, project.id)
        active_tracks = [t for t in all_tracks if t.status == TrackStatus.ACTIVE]

        created_at = _dt.datetime.now(_dt.UTC).isoformat()

        for track in active_tracks:
            processed += 1
            logger.info(
                "encoder.track",
                track_id=track.id,
                path=track.relative_path,
            )

            features = FeatureRepository.get_latest_features(session, track.id)
            if not features:
                failed += 1
                logger.warning(
                    "encoder.validation_failed",
                    track_id=track.id,
                    error="No features found",
                )
                continue

            try:
                dna = builder.build(
                    track_id=track.id,
                    features=features,
                    created_at=created_at,
                )
                encoder.encode(dna, created_at=created_at)
                encoded_count += 1
            except MusicDNAError as exc:
                failed += 1
                logger.warning(
                    "encoder.validation_failed",
                    track_id=track.id,
                    error=str(exc),
                )
                continue
    finally:
        session.close()

    console.print()
    console.print("[bold cyan]Encoding Summary[/bold cyan]")
    console.print(f"  Tracks processed:  {processed}")
    console.print(f"  Encoded:           {encoded_count}")
    console.print(f"  Failed:            {failed}")
    console.print(f"  Vector dimension:  {encoder.layout.dimension()}")
    console.print(f"  Schema version:    {MusicDNAEncoder.SCHEMA_VERSION}")
    console.print()

    logger.info(
        "encoder.completed",
        processed=processed,
        encoded=encoded_count,
        failed=failed,
    )


@app.command()
def compare(
    track_a: str = typer.Option(..., "--track-a", help="UUID of the first track."),
    track_b: str = typer.Option(..., "--track-b", help="UUID of the second track."),
    metric: str = typer.Option(
        "cosine", "--metric", help="Metric: cosine, euclidean, or manhattan."
    ),
) -> None:
    """Compare two tracks by Music DNA similarity."""
    import datetime as _dt

    settings, logger = _bootstrap()

    logger.info("similarity.started", track_a=track_a, track_b=track_b, metric=metric)
    logger.info("similarity.metric", metric=metric)

    feature_registry = build_default_feature_registry()
    builder = MusicDNABuilder(feature_registry)
    encoder = MusicDNAEncoder()
    engine = SimilarityEngine()

    session_factory = get_session_factory()
    session = session_factory()
    try:
        created_at = _dt.datetime.now(_dt.UTC).isoformat()

        track_a_obj = session.get(Track, track_a)
        track_b_obj = session.get(Track, track_b)

        if track_a_obj is None or track_b_obj is None:
            logger.warning(
                "similarity.validation_failed",
                error="Track not found",
            )
            console.print("[red]Error: One or both tracks not found.[/red]")
            raise typer.Exit(1)

        features_a = FeatureRepository.get_latest_features(session, track_a_obj.id)
        features_b = FeatureRepository.get_latest_features(session, track_b_obj.id)

        if not features_a or not features_b:
            logger.warning(
                "similarity.validation_failed",
                error="No features found for one or both tracks",
            )
            console.print("[red]Error: No features found for one or both tracks.[/red]")
            raise typer.Exit(1)

        try:
            dna_a = builder.build(
                track_id=track_a_obj.id,
                features=features_a,
                created_at=created_at,
            )
            dna_b = builder.build(
                track_id=track_b_obj.id,
                features=features_b,
                created_at=created_at,
            )
            vec_a = encoder.encode(dna_a, created_at=created_at)
            vec_b = encoder.encode(dna_b, created_at=created_at)

            result = engine.compare(vec_a, vec_b, metric=metric)
        except (MusicDNAError, SimilarityError) as exc:
            logger.warning(
                "similarity.validation_failed",
                error=str(exc),
            )
            console.print(f"[red]Error: {exc}[/red]")
            raise typer.Exit(1) from exc

        console.print()
        console.print("[bold cyan]Similarity Summary[/bold cyan]")
        console.print(f"  Metric:         {metric.capitalize()}")
        console.print(f"  Similarity:     {result.similarity_score:.4f}")
        console.print(f"  Distance:       {result.distance:.4f}")
        console.print(f"  Confidence:     {result.confidence:.3f}")
        console.print()
        console.print("  Per Category:")
        for cat, score in result.per_category_scores.items():
            console.print(f"    {cat.capitalize():<14} {score:.4f}")
        console.print()
        console.print("  Top Contributors:")
        for contributor in result.top_contributors:
            console.print(f"    {contributor}")
        console.print()

        logger.info(
            "similarity.completed",
            track_a=track_a,
            track_b=track_b,
            metric=metric,
            similarity=result.similarity_score,
            distance=result.distance,
        )
    finally:
        session.close()


if __name__ == "__main__":
    app()
