"""Read encrypted audio from archived meetings.

Mirror of MeetingAudioWriter - reads encrypted PCM16 chunks and converts to float32.
Reuses ChunkedAssetReader from security/crypto.py for decryption.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import TYPE_CHECKING

import numpy as np

from noteflow.config.constants import DEFAULT_SAMPLE_RATE
from noteflow.infrastructure.audio.dto import TimestampedAudio
from noteflow.infrastructure.logging import get_logger
from noteflow.infrastructure.security.crypto import ChunkedAssetReader

if TYPE_CHECKING:
    from noteflow.infrastructure.security.crypto import AesGcmCryptoBox

logger = get_logger(__name__)


class MeetingAudioReader:
    """Read audio chunks from encrypted meeting file.

    Mirror of MeetingAudioWriter - handles manifest parsing, DEK unwrapping,
    and encrypted audio decryption.

    Directory structure (as created by MeetingAudioWriter):
        ~/.noteflow/meetings/<meeting-uuid>/
        ├── manifest.json    # Meeting metadata + wrapped DEK
        └── audio.enc       # Encrypted PCM16 chunks (NFAE format)
    """

    def __init__(
        self,
        crypto: AesGcmCryptoBox,
        meetings_dir: Path,
    ) -> None:
        """Initialize audio reader.

        Args:
            crypto: CryptoBox instance for decryption and DEK unwrapping.
            meetings_dir: Root directory for all meetings (e.g., ~/.noteflow/meetings).
        """
        self._crypto = crypto
        self._meetings_dir = meetings_dir
        self._meeting_dir: Path | None = None
        self._sample_rate: int = DEFAULT_SAMPLE_RATE

    def load_meeting_audio(
        self,
        meeting_id: str,
        asset_path: str | None = None,
    ) -> list[TimestampedAudio]:
        """Load all audio from an archived meeting.

        Reads manifest, unwraps DEK, decrypts audio chunks, converts to float32.

        Args:
            meeting_id: Meeting UUID string.
            asset_path: Relative path for audio storage (defaults to meeting_id).
                Use the stored asset_path from the database when available.

        Returns:
            List of TimestampedAudio chunks (or empty list if not found/failed).

        Raises:
            FileNotFoundError: If meeting directory or audio file not found.
            ValueError: If manifest is invalid or audio format unsupported.
        """
        storage_path = asset_path or meeting_id
        meeting_dir = self._meetings_dir / storage_path
        self._meeting_dir = meeting_dir

        # Load and parse manifest
        manifest_path = meeting_dir / "manifest.json"
        if not manifest_path.exists():
            raise FileNotFoundError(f"Manifest not found: {manifest_path}")

        manifest = json.loads(manifest_path.read_text())
        self._sample_rate = manifest.get("sample_rate", DEFAULT_SAMPLE_RATE)
        wrapped_dek_hex = manifest.get("wrapped_dek")

        if not wrapped_dek_hex:
            raise ValueError("Manifest missing wrapped_dek")

        # Unwrap DEK
        wrapped_dek = bytes.fromhex(wrapped_dek_hex)
        dek = self._crypto.unwrap_dek(wrapped_dek)

        # Open encrypted audio file
        audio_path = meeting_dir / "audio.enc"
        if not audio_path.exists():
            raise FileNotFoundError(f"Audio file not found: {audio_path}")

        reader = ChunkedAssetReader(self._crypto)
        reader.open(audio_path, dek)

        try:
            return self._read_all_chunks(reader)
        finally:
            reader.close()

    def _read_all_chunks(
        self,
        reader: ChunkedAssetReader,
    ) -> list[TimestampedAudio]:
        """Read and convert all audio chunks.

        Args:
            reader: Open ChunkedAssetReader.

        Returns:
            List of TimestampedAudio chunks.
        """
        chunks: list[TimestampedAudio] = []
        current_time = 0.0

        for chunk_bytes in reader.read_chunks():
            # Convert PCM16 bytes back to int16 array
            pcm16 = np.frombuffer(chunk_bytes, dtype=np.int16)

            # Convert int16 [-32768, 32767] to float32 [-1.0, 1.0]
            audio_float = pcm16.astype(np.float32) / 32767.0

            # Calculate duration based on sample rate
            duration = len(audio_float) / self._sample_rate

            chunks.append(
                TimestampedAudio(
                    frames=audio_float,
                    timestamp=current_time,
                    duration=duration,
                )
            )

            current_time += duration

        logger.info(
            "Loaded audio: meeting_dir=%s, chunks=%d, total_duration=%.2fs",
            self._meeting_dir,
            len(chunks),
            current_time,
        )

        return chunks

    def get_manifest(
        self,
        meeting_id: str,
        asset_path: str | None = None,
    ) -> dict[str, object] | None:
        """Get manifest metadata for a meeting.

        Args:
            meeting_id: Meeting UUID string.
            asset_path: Relative path for audio storage (defaults to meeting_id).

        Returns:
            Manifest dict or None if not found.
        """
        storage_path = asset_path or meeting_id
        manifest_path = self._meetings_dir / storage_path / "manifest.json"
        if not manifest_path.exists():
            return None

        return dict(json.loads(manifest_path.read_text()))

    def audio_exists(
        self,
        meeting_id: str,
        asset_path: str | None = None,
    ) -> bool:
        """Check if audio file exists for a meeting.

        Args:
            meeting_id: Meeting UUID string.
            asset_path: Relative path for audio storage (defaults to meeting_id).

        Returns:
            True if audio.enc exists.
        """
        storage_path = asset_path or meeting_id
        meeting_dir = self._meetings_dir / storage_path
        audio_path = meeting_dir / "audio.enc"
        manifest_path = meeting_dir / "manifest.json"
        return audio_path.exists() and manifest_path.exists()

    @property
    def sample_rate(self) -> int:
        """Return the sample rate from the last loaded manifest."""
        return self._sample_rate
