"""alice teaparty — Podcast-style audio generation from source material."""

from __future__ import annotations

import contextlib
import json
import platform
import subprocess
import sys
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

import click
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
from rich.table import Table

from alice_cli import personality
from alice_cli.auth import resolve_username
from alice_cli.aws.bedrock_context import create_bedrock_context
from alice_cli.aws.session import get_session
from alice_cli.console import (
    err_console,
    print_banner,
    print_rule,
    print_success,
    spinner,
)
from alice_cli.constants import get_memory_bucket
from alice_cli.errors import AliceCLIError
from alice_cli.models import resolve_model_id
from alice_cli.session_record import build_session_record
from alice_cli.teaparty.audio_locker import (
    download_audio,
    publish_audio,
    resolve_audio_bucket,
    unpublish_audio,
    upload_audio,
)
from alice_cli.teaparty.audio_stitcher import stitch_audio
from alice_cli.teaparty.models import (
    GenerationMode,
    Script,
    TeaPartyMetadata,
    parse_script,
    serialize_script,
)
from alice_cli.teaparty.script_generator import generate_script
from alice_cli.teaparty.source_loader import load_sources
from alice_cli.teaparty.speech_synthesizer import select_engine, synthesize_turns
from alice_cli.teaparty.voice_resolver import resolve_voices

if TYPE_CHECKING:
    from alice_cli.locker import CloudLocker
    from alice_cli.models import CLIConfig


# ---------------------------------------------------------------------------
# Retrieval helpers
# ---------------------------------------------------------------------------


def _list_sessions(locker: CloudLocker, quiet: bool) -> None:
    """Display a table of Tea Party sessions from the Cloud Locker index."""
    with spinner("Loading session index…", quiet):
        index = locker.load_index()

    entries = [e for e in index.entries if e.type == "teaparty"]
    entries.sort(key=lambda e: e.timestamp, reverse=True)

    if not entries:
        err_console.print("  [dim]No Tea Party sessions found.[/dim]")
        return

    table = Table(title="Tea Party Sessions", show_lines=False)
    table.add_column("ID", style="cyan", no_wrap=True)
    table.add_column("Timestamp", style="cyan", no_wrap=True)
    table.add_column("Model", style="green")
    table.add_column("Prompt Preview", style="white")
    table.add_column("Published", style="yellow")

    for entry in entries[:20]:
        table.add_row(
            entry.id[:8],
            entry.timestamp[:19],
            entry.model,
            entry.prompt_preview[:60],
            "—",
        )

    err_console.print(table)
    print_success(f"Showing {min(len(entries), 20)} session(s)", quiet)


def _get_audio(
    locker: CloudLocker,
    session_id: str,
    session: Any,
    output: str | None,
    quiet: bool,
) -> None:
    """Download an MP3 from the Audio_Bucket."""
    record = _load_teaparty_record(locker, session_id)
    meta = record.metadata or {}
    bucket = meta.get("audio_bucket")
    key = meta.get("artifact_key")
    if not bucket or not key:
        raise AliceCLIError(f"Session {session_id} has no audio artifact metadata.")

    with spinner("Downloading audio…", quiet):
        audio_data = download_audio(session, bucket, key)

    out_path = output or f"teaparty-{session_id[:8]}.mp3"
    Path(out_path).write_bytes(audio_data)
    size_kb = len(audio_data) / 1024
    click.echo(out_path)
    print_success(f"Downloaded {size_kb:.1f} KB → {out_path}", quiet)


def _show_session(locker: CloudLocker, session_id: str, quiet: bool) -> None:
    """Display full Podcast_Record metadata and script."""
    record = _load_teaparty_record(locker, session_id)

    print_rule(quiet)
    err_console.print(f"  [bold]Session:[/bold] {record.id}")
    err_console.print(f"  [bold]Type:[/bold]    {record.type}")
    err_console.print(f"  [bold]Time:[/bold]    {record.timestamp}")
    err_console.print(f"  [bold]Model:[/bold]   {record.model}")
    print_rule(quiet)

    if record.prompt:
        err_console.print("\n[bold]Prompt (source preview):[/bold]")
        err_console.print(record.prompt[:500])

    if record.response:
        err_console.print("\n[bold]Script:[/bold]")
        err_console.print(record.response)

    if record.tokens:
        err_console.print(
            f"\n[dim]Tokens: {record.tokens.input} in / "
            f"{record.tokens.output} out / {record.tokens.total} total[/dim]"
        )

    if record.metadata:
        err_console.print("\n[bold]Metadata:[/bold]")
        err_console.print(json.dumps(record.metadata, indent=2, ensure_ascii=False))

    err_console.print(f"\n[dim]Download MP3: alice teaparty --get {record.id}[/dim]")
    print_rule(quiet)


def _replay_audio(
    locker: CloudLocker,
    session_id: str,
    session: Any,
    quiet: bool,
) -> None:
    """Download MP3 to a temp file and open with system audio player."""
    record = _load_teaparty_record(locker, session_id)
    meta = record.metadata or {}
    bucket = meta.get("audio_bucket")
    key = meta.get("artifact_key")
    if not bucket or not key:
        raise AliceCLIError(f"Session {session_id} has no audio artifact metadata.")

    with spinner("Downloading audio for replay…", quiet):
        audio_data = download_audio(session, bucket, key)

    with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp:
        tmp.write(audio_data)
        tmp_path = tmp.name

    opener = "open" if platform.system() == "Darwin" else "xdg-open"
    try:
        subprocess.Popen([opener, tmp_path])  # noqa: S603
    except FileNotFoundError as err:
        raise AliceCLIError(
            f"Cannot open audio player ({opener}). File saved at: {tmp_path}"
        ) from err

    print_success(f"Playing {tmp_path}", quiet)


def _publish_session(
    locker: CloudLocker,
    session_id: str,
    session: Any,
    config: CLIConfig,
    expires: int | None,
    audio_bucket_override: str | None,
    quiet: bool,
) -> str:
    """Publish a Tea Party MP3 and return the URL."""
    record = _load_teaparty_record(locker, session_id)
    meta = record.metadata or {}
    bucket = meta.get("audio_bucket") or resolve_audio_bucket(config, audio_bucket_override)
    key = meta.get("artifact_key")
    if not key:
        raise AliceCLIError(f"Session {session_id} has no artifact_key in metadata.")

    with spinner("Publishing audio…", quiet):
        url = publish_audio(session, bucket, key, session_id, expires)

    # Update metadata with published info
    meta["published_url"] = url
    meta["published_at"] = datetime.now(UTC).isoformat()
    record.metadata = meta
    with contextlib.suppress(Exception):
        locker.persist(record, "teaparty")

    click.echo(url)
    print_success(f"Published: {url}", quiet)
    return url


def _unpublish_session(
    locker: CloudLocker,
    session_id: str,
    session: Any,
    config: CLIConfig,
    audio_bucket_override: str | None,
    quiet: bool,
) -> None:
    """Unpublish a Tea Party MP3."""
    record = _load_teaparty_record(locker, session_id)
    meta = record.metadata or {}
    bucket = meta.get("audio_bucket") or resolve_audio_bucket(config, audio_bucket_override)

    with spinner("Unpublishing audio…", quiet):
        unpublish_audio(session, bucket, session_id)

    # Clear published metadata
    meta.pop("published_url", None)
    meta.pop("published_at", None)
    record.metadata = meta
    with contextlib.suppress(Exception):
        locker.persist(record, "teaparty")

    print_success(f"Unpublished session {session_id[:8]}", quiet)


def _load_teaparty_record(locker: CloudLocker, session_id: str) -> Any:
    """Load a teaparty session record from the Cloud Locker."""
    try:
        return locker.load_session(session_id, "teaparty")
    except Exception as exc:
        raise AliceCLIError(f"Tea Party session {session_id} not found.") from exc


# ---------------------------------------------------------------------------
# Click command
# ---------------------------------------------------------------------------


@click.command("teaparty")
@click.argument("source", nargs=-1)
@click.option("--model", "model_id", default="sonnet")
@click.option("--output", type=click.Path())
@click.option("--voice-a", default="Matthew")
@click.option("--voice-b", default="Ruth")
@click.option("--no-save", is_flag=True)
@click.option("--pause-ms", default=500, type=int)
@click.option("--quiet", is_flag=True)
@click.option(
    "--mode",
    type=click.Choice(["conversation", "narrator", "briefing", "interview"]),
    default="conversation",
)
@click.option("--lang", default="en-US")
@click.option("--neural-style", is_flag=True)
@click.option("--rehearse", type=click.Path(exists=False))
@click.option("--audio-bucket")
@click.option("--list", "list_sessions", is_flag=True)
@click.option("--get", "get_id")
@click.option("--show", "show_id")
@click.option("--replay", "replay_id")
@click.option("--publish", "publish_id")
@click.option("--unpublish", "unpublish_id")
@click.option("--expires", type=int, default=None)
@click.pass_context
def teaparty_cmd(
    ctx: click.Context,
    source: tuple[str, ...],
    model_id: str,
    output: str | None,
    voice_a: str,
    voice_b: str,
    no_save: bool,
    pause_ms: int,
    quiet: bool,
    mode: str,
    lang: str,
    neural_style: bool,
    rehearse: str | None,
    audio_bucket: str | None,
    list_sessions: bool,
    get_id: str | None,
    show_id: str | None,
    replay_id: str | None,
    publish_id: str | None,
    unpublish_id: str | None,
    expires: int | None,
) -> None:
    """Generate podcast-style audio from source material.

    \b
    Examples:
        alice teaparty paper.txt
        alice teaparty docs/*.md --mode briefing
        cat notes.txt | alice teaparty -
        alice teaparty locker:abc123 --voice-a Danielle
        alice teaparty --rehearse script.json
        alice teaparty --list
        alice teaparty --get <session-id>
        alice teaparty --show <session-id>
        alice teaparty --replay <session-id>
        alice teaparty --publish <session-id>
        alice teaparty --unpublish <session-id>
    """
    config: CLIConfig = ctx.obj["config"]
    # Merge quiet flags: global --quiet or local --quiet
    quiet = quiet or config.quiet

    print_banner(personality.BANNER, quiet)

    # ── Authenticate ──────────────────────────────────────────────────
    username = resolve_username(config)

    # ── Build boto3 session for S3/Polly ──────────────────────────────
    boto_session = _get_boto_session(config)

    # ── Cloud Locker for session persistence ──────────────────────────
    from alice_cli.locker import CloudLocker

    locker = CloudLocker(
        config=config,
        username=username,
        bucket=get_memory_bucket(config.namespace, config.environment),
    )

    # ── Early-return retrieval branches ───────────────────────────────
    if list_sessions:
        _list_sessions(locker, quiet)
        return

    if get_id:
        _get_audio(locker, get_id, boto_session, output, quiet)
        return

    if show_id and not rehearse:
        _show_session(locker, show_id, quiet)
        return

    if replay_id:
        _replay_audio(locker, replay_id, boto_session, quiet)
        return

    if publish_id:
        _publish_session(locker, publish_id, boto_session, config, expires, audio_bucket, quiet)
        return

    if unpublish_id:
        _unpublish_session(locker, unpublish_id, boto_session, config, audio_bucket, quiet)
        return

    # ── Generation pipeline ───────────────────────────────────────────
    gen_mode = GenerationMode(mode)

    # Rehearse mode: --rehearse with --show ID extracts script from record
    if rehearse and show_id:
        record = _load_teaparty_record(locker, show_id)
        if not record.response:
            raise AliceCLIError(f"Session {show_id} has no script in response field.")
        script = parse_script(record.response)
        source_text = record.prompt or ""
        tokens = record.tokens
        rehearse_from = show_id
    elif rehearse:
        # Read script from file
        rehearse_path = Path(rehearse)
        if not rehearse_path.exists():
            raise AliceCLIError(f"Rehearse file not found: {rehearse}")
        script_json = rehearse_path.read_text("utf-8")
        script = parse_script(script_json)
        source_text = ""
        tokens = None
        rehearse_from = rehearse
    else:
        # Full generation: require source
        if not source and sys.stdin.isatty():
            raise AliceCLIError(
                "Source material is required. Provide file paths, pipe stdin, "
                "or use locker:/doi:/s3://url: prefixes.\n"
                "  Example: alice teaparty paper.txt\n"
                "  Example: cat notes.txt | alice teaparty -"
            )

        # 1. Load source material
        with spinner("Loading source material…", quiet):
            source_text = load_sources(source, config, locker, boto_session)

        # 2. Generate script via Bedrock
        bedrock_ctx = create_bedrock_context(config)
        resolved_id = resolve_model_id(model_id, bedrock_ctx.inference_profile_arns)

        with spinner(f"Generating script with {model_id}…", quiet):
            gen_result = generate_script(
                source_text=source_text,
                client=bedrock_ctx.client,
                model_id=resolved_id,
                mode=gen_mode,
                lang=lang,
            )

        script = gen_result.script
        tokens = gen_result.tokens
        rehearse_from = None

    # 3. Resolve voices
    with spinner("Resolving voices…", quiet):
        resolved = resolve_voices(
            boto_session,
            lang=lang,
            voice_a_override=voice_a if voice_a != "Matthew" else None,
            voice_b_override=voice_b if voice_b != "Ruth" else None,
        )

    if resolved.warning and not quiet:
        err_console.print(f"  [yellow]⚠ {resolved.warning}[/yellow]")

    final_voice_a = voice_a if voice_a != "Matthew" else resolved.voice_a
    final_voice_b = voice_b if voice_b != "Ruth" else resolved.voice_b

    # 4. Select engine per voice
    engine = select_engine(gen_mode, final_voice_a, resolved.voice_a_engines, neural_style)

    # 5. Synthesize turns
    if quiet:
        synth_segments = synthesize_turns(
            script=script,
            session=boto_session,
            voice_a=final_voice_a,
            voice_b=final_voice_b,
            engine=engine,
            mode=gen_mode,
            neural_style=neural_style,
            quiet=quiet,
        )
    else:
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            BarColumn(),
            TextColumn("{task.completed}/{task.total}"),
            console=err_console,
        ) as progress:
            task = progress.add_task("Synthesizing speech…", total=len(script.turns))
            synth_segments = []
            for _idx, turn in enumerate(script.turns):
                seg = synthesize_turns(
                    script=Script.model_construct(turns=[turn]),
                    session=boto_session,
                    voice_a=final_voice_a,
                    voice_b=final_voice_b,
                    engine=engine,
                    mode=gen_mode,
                    neural_style=neural_style,
                    quiet=True,
                )
                synth_segments.extend(seg)
                progress.update(task, advance=1)

    # 6. Stitch audio
    segments_bytes = [s.audio_data for s in synth_segments]
    speaker_ids: list[str] = [
        t.speaker if gen_mode != GenerationMode.NARRATOR else "Host_A" for t in script.turns
    ]
    turn_texts = [t.text for t in script.turns]

    timestamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
    output_path = output or f"teaparty-{timestamp}.mp3"

    with spinner("Stitching audio…", quiet):
        duration = stitch_audio(
            segments=segments_bytes,
            speaker_ids=speaker_ids,
            turn_texts=turn_texts,
            output_path=output_path,
            pause_ms=pause_ms,
        )

    # 7. Upload to Audio_Bucket
    bucket = resolve_audio_bucket(config, audio_bucket)
    session_record = None
    artifact_key = f"{username}/teaparty/{timestamp}.mp3"

    try:
        audio_data = Path(output_path).read_bytes()
        with spinner("Uploading to Audio Locker…", quiet):
            upload_audio(boto_session, bucket, artifact_key, audio_data)
    except AliceCLIError as exc:
        if not quiet:
            err_console.print(f"  [yellow]⚠ Audio upload failed: {exc}. Local file saved.[/yellow]")

    # 8. Persist Podcast_Record to Cloud Locker
    if not no_save:
        from alice_cli.models import TokenUsage

        meta = TeaPartyMetadata(
            artifact_key=artifact_key,
            audio_bucket=bucket,
            voice_a=final_voice_a,
            voice_b=final_voice_b,
            duration_seconds=duration,
            turn_count=len(script.turns),
            mode=mode,
            lang=lang,
            rehearsed_from=rehearse_from if rehearse else None,
        )

        record_tokens = tokens or TokenUsage(input=0, output=0, total=0)
        session_record = build_session_record(
            record_type="teaparty",
            model=model_id,
            prompt=source_text[:2000],
            response=serialize_script(script),
            tokens=record_tokens,
            metadata=meta.model_dump(),
        )

        with spinner("Saving session…", quiet):
            locker.persist(session_record, "teaparty")

    # 9. Publish during generation if --publish flag (no ID) was used
    if publish_id == "" and session_record:
        _publish_session(
            locker, session_record.id, boto_session, config, expires, audio_bucket, quiet
        )

    # ── Success output ────────────────────────────────────────────────
    mins = int(duration) // 60
    secs = int(duration) % 60
    click.echo(output_path)
    print_success(
        f"Generated {mins}m {secs}s podcast ({len(script.turns)} turns) → {output_path}",
        quiet,
    )
    if tokens and not quiet:
        err_console.print(
            f"  [dim]Tokens: {tokens.input} in / {tokens.output} out / {tokens.total} total[/dim]"
        )
    print_rule(quiet)


def _get_boto_session(config: CLIConfig) -> Any:
    """Create a boto3 session from the CLI config."""
    if config.bedrock_secret_key and not config.profile:
        return get_session(
            region=config.region,
            access_key=config.bedrock_access_key or "",
            secret_key=config.bedrock_secret_key or "",
        )
    if config.profile:
        return get_session(profile=config.profile, region=config.region)
    return get_session(region=config.region)
