"""
Utilities for AI feedback analysis: word list loading, transcription, and metrics.
"""

from __future__ import annotations

import asyncio
import contextlib
import os
import re
import tempfile
import threading
import time
from dataclasses import dataclass
from functools import lru_cache
from typing import Any

import cv2
from faster_whisper import WhisperModel
from moviepy import VideoFileClip
from pydub import AudioSegment
from pydub.silence import detect_silence

from app.core.logging import get_logger

logger = get_logger("analysis_utils")

# Precomputed paths and compiled regexes for speed
_APP_DIR = os.path.dirname(os.path.dirname(__file__))  # app/
_RESOURCES_DIR = os.path.join(_APP_DIR, "resources")
_NON_ALPHA_RE = re.compile(r"[^a-zA-Z']")
_WORD_RE = re.compile(r"\b\w+\b")

# Lazy-initialized global Whisper model (heavy to load)
_WHISPER_MODEL = None
_WHISPER_MODEL_LOCK = threading.Lock()


# -----------------------------
# Helpers and data structures
# -----------------------------


def _get_resource_path_internal(*parts: str) -> str:
    """Internal fast path join into `app/resources`. Module-private."""
    return os.path.join(_RESOURCES_DIR, *parts)


@lru_cache(maxsize=32)
def _load_word_list_internal(file_name: str) -> list[str]:
    """Internal cached word list loader. Module-private."""
    path = _get_resource_path_internal("word_lists", file_name)
    if not os.path.exists(path):
        logger.warning(f"Word list file not found: {path}")
        return []

    try:
        with open(path, encoding="utf-8") as f:
            content = f.read().strip()

        # Legacy lists are comma-separated
        words = [w.strip().lower() for w in content.split(",") if w.strip()]
        return words

    except Exception as e:
        logger.error(f"Failed to load word list {file_name}: {e}")
        return []


## Removed unused helpers: analyze_filler_phrases, analyze_power_phrases,
## total_words_from_text, compute_filler_percentage


def _format_mmss_internal(seconds: float) -> str:
    """Internal seconds to MM:SS. Module-private."""
    if seconds < 0:
        seconds = 0
    minutes = int(seconds // 60)
    secs = int(seconds % 60)
    return f"{minutes:>2d}:{secs:02d}"


@dataclass
class TranscriptionWord:
    text: str
    start: float
    end: float


@dataclass
class TranscriptionSegment:
    text: str
    start: float
    end: float
    words: list[TranscriptionWord]


## Removed unused analyze_audio_basic helper (superseded by AudioProcessor implementation)


def _count_words_and_phrases_internal(
    full_text: str,
    segments: list[TranscriptionSegment],
    filler_list: list[str],
    filler_phrases: list[str],
    power_list: list[str],
    power_phrases: list[str],
    negative_list: list[str],
    um_list: list[str],
) -> dict[str, object]:
    text_lower = full_text.lower()

    def count_phrases(phrases: list[str]) -> tuple[int, list[dict[str, int]]]:
        total = 0
        items: list[dict[str, int]] = []
        for p in phrases:
            p = p.strip().lower()
            if not p:
                continue
            matches = text_lower.count(p)
            if matches > 0:
                total += matches
                items.append({p: matches})
        return total, items

    filler_phrase_total, filler_phrase_items = count_phrases(filler_phrases)
    power_phrase_total, power_phrase_items = count_phrases(power_phrases)

    def word_time_map(
        targets: set[str],
    ) -> tuple[int, dict[str, int], list[dict[str, list[str]]]]:
        total = 0
        counts: dict[str, int] = {}
        timeline: dict[str, list[str]] = {}
        for seg in segments:
            for w in seg.words:
                token = _NON_ALPHA_RE.sub("", w.text.lower())
                if not token:
                    continue
                if token in targets:
                    total += 1
                    counts[token] = counts.get(token, 0) + 1
                    timeline.setdefault(token, []).append(
                        _format_mmss_internal(w.start)
                    )
        timeline_list = [{k: v} for k, v in timeline.items()]
        return total, counts, timeline_list

    filler_targets = set(filler_list)
    power_targets = set(power_list)
    negative_targets = set(negative_list)
    um_targets = set(um_list)

    filler_total, filler_counts, filler_timeline = word_time_map(filler_targets)
    power_total, power_counts, power_timeline = word_time_map(power_targets)
    negative_total, negative_counts, negative_timeline = word_time_map(negative_targets)
    um_total, um_counts, um_timeline = word_time_map(um_targets)

    total_words = len(_WORD_RE.findall(text_lower))

    return {
        "total_words": total_words,
        "filler_word_count": filler_total,
        "power_word_count": power_total,
        "negative_word_count": negative_total,
        "um_word_count": um_total,
        "filler_word_timestamp": filler_timeline,
        "filler_count": filler_counts,
        "filler_phrases": filler_phrase_items,
        "filler_phrase_counter": filler_phrase_total,
        "power_phrases": power_phrase_items,
        "power_phrase_counter": power_phrase_total,
        "power_word_timestamp": power_timeline,
        "power_count": power_counts,
        "negative_word_timestamp": negative_timeline,
        "negative_count": negative_counts,
        "um_word_timestamp": um_timeline,
        "um_count": um_counts,
    }


# AnalysisUtils class to wrap existing functions
class AnalysisUtils:
    """Utility class for analysis operations."""

    def __init__(self):
        pass

    async def detect_silence_legacy(self, audio: AudioSegment) -> list[tuple[int, int]]:
        """
        Detect silence using legacy parameters exactly matching the old system.

        Legacy parameters:
        - min_silence_len=3200 (3.2 seconds)
        - silence_thresh=-40 dB

        Args:
            audio: AudioSegment object

        Returns:
            List of (start_ms, end_ms) silence ranges
        """
        try:
            # Use legacy parameters exactly as in the old system
            silence_ranges = await asyncio.to_thread(
                detect_silence,
                audio,
                min_silence_len=3200,  # 3.2 seconds
                silence_thresh=-40,  # -40 dB
            )
            # Legacy behavior: ignore an initial 0:00 pause marker
            silence_ranges = [
                (start, end) for start, end in silence_ranges if start > 0
            ]
            return silence_ranges

        except Exception:
            return []

    async def extract_audio_from_video(self, video_path: str) -> AudioSegment:
        """Extract audio from video file."""
        try:

            def _work() -> AudioSegment:
                # Use moviepy to extract audio
                video = VideoFileClip(video_path)
                with tempfile.NamedTemporaryFile(
                    suffix=".wav", delete=False
                ) as temp_file:
                    temp_audio_path = temp_file.name
                try:
                    audio = video.audio
                    if audio is None:
                        # Create a silent audio segment when no audio stream is found
                        logger.warning(
                            f"No audio stream found in video file: {video_path}"
                        )
                        # Create a 1-second silent audio segment (duration will be adjusted later if needed)
                        audio_segment = AudioSegment.silent(
                            duration=1000
                        )  # 1000ms = 1 second
                        return audio_segment
                    # Some versions of moviepy.AudioClip.write_audiofile do not accept 'verbose'
                    # Use logger=None only to suppress logging
                    audio.write_audiofile(temp_audio_path, logger=None)
                    # Load with pydub
                    audio_segment = AudioSegment.from_wav(temp_audio_path)
                    return audio_segment
                finally:
                    with contextlib.suppress(Exception):
                        video.close()
                    with contextlib.suppress(Exception):
                        os.remove(temp_audio_path)

            return await asyncio.to_thread(_work)

        except Exception:
            raise

    async def extract_audio_from_file(self, file_path: str) -> AudioSegment:
        """Extract audio from audio file or video file."""
        try:
            # Check if it's a video file
            if file_path.lower().endswith((".mp4", ".avi", ".mov", ".mkv")):
                # Extract audio from video
                return await self.extract_audio_from_video(file_path)
            else:
                # Load audio file directly
                return await asyncio.to_thread(AudioSegment.from_file, file_path)
        except Exception:
            raise

    def is_audio_file(self, file_path: str) -> bool:
        """Check if file is an audio file."""
        audio_extensions = {".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg"}
        return any(file_path.lower().endswith(ext) for ext in audio_extensions)

    def is_video_file(self, file_path: str) -> bool:
        """Check if file is a video file."""
        video_extensions = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".flv"}
        return any(file_path.lower().endswith(ext) for ext in video_extensions)

    async def get_video_properties(self, video_path: str) -> dict[str, Any]:
        """Get video properties like frame count, FPS, etc."""
        try:

            def _work() -> dict[str, Any]:
                cap = cv2.VideoCapture(video_path)
                try:
                    if not cap.isOpened():
                        raise Exception("Failed to open video file")
                    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
                    fps = cap.get(cv2.CAP_PROP_FPS)
                    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
                    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
                    return {
                        "total_frames": total_frames,
                        "fps": fps,
                        "width": width,
                        "height": height,
                    }
                finally:
                    cap.release()

            return await asyncio.to_thread(_work)

        except Exception:
            return {}

    async def transcribe_audio_with_timestamps(
        self, audio: AudioSegment
    ) -> dict[str, Any]:
        """Transcribe audio with word-level timestamps using faster-whisper, returning legacy-like text."""
        try:
            # Save audio to a unique temporary file to avoid collisions under concurrency
            with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
                temp_path = tmp.name
            await asyncio.to_thread(audio.export, temp_path, format="wav")

            # Use global cached faster-whisper model (heavy to load)
            model = _get_whisper_model()
            logger.info(
                f"Whisper transcription starting: file={os.path.basename(temp_path)} model=base device=cpu compute=int8"
            )
            t0 = time.monotonic()
            segments, info = await asyncio.to_thread(
                model.transcribe, temp_path, word_timestamps=True
            )

            # Extract words with timestamps and accumulate full text similar to legacy `reslt`
            words = []
            text_parts = []
            for segment in segments:
                seg_text = (segment.text or "").strip()
                if seg_text:
                    text_parts.append(seg_text)
                for word in segment.words:
                    words.append(
                        {
                            "word": word.word,
                            "start": word.start,
                            "end": word.end,
                            "probability": word.probability,
                        }
                    )
            full_text = " ".join(text_parts).strip()

            elapsed = time.monotonic() - t0
            logger.info(
                f"Whisper transcription completed: lang={getattr(info, 'language', 'unknown')} prob={getattr(info, 'language_probability', 0):.2f} segments={len(text_parts)} words={len(words)} elapsed={elapsed:.2f}s"
            )

            # Clean up
            with contextlib.suppress(Exception):
                os.remove(temp_path)

            return {
                "words": words,
                "text": full_text,
                "language": info.language,
                "language_probability": info.language_probability,
            }

        except Exception:
            logger.error("Whisper transcription failed", exc_info=True)
            return {"words": [], "text": ""}

    def seconds_to_mmss(self, seconds: float) -> str:
        """Convert seconds to legacy MM:SS, delegating to shared helper."""
        return _format_mmss_internal(seconds)

    def classify_volume(self, dbfs: float) -> dict[str, Any]:
        """Classify audio volume into legacy buckets and normalize extreme lows.

        Returns a dict with keys: category (str) and normalized_dbfs (float)
        """
        normalized = dbfs
        if normalized < -60:
            normalized = -100
        if normalized > -10:
            category = "loud"
        elif normalized > -30:
            category = "Normal"
        else:
            category = "Low"
        return {"category": category, "normalized_dbfs": normalized}

    # Static, class-style accessors for pure helpers (Option B)
    @staticmethod
    def get_resource_path(*parts: str) -> str:
        return _get_resource_path_internal(*parts)

    @staticmethod
    def load_word_list(file_name: str) -> list[str]:
        return _load_word_list_internal(file_name)

    @staticmethod
    def count_words_and_phrases(
        full_text: str,
        segments: list[TranscriptionSegment],
        filler_list: list[str],
        filler_phrases: list[str],
        power_list: list[str],
        power_phrases: list[str],
        negative_list: list[str],
        um_list: list[str],
    ) -> dict[str, object]:
        return _count_words_and_phrases_internal(
            full_text,
            segments,
            filler_list,
            filler_phrases,
            power_list,
            power_phrases,
            negative_list,
            um_list,
        )

    # -----------------------------
    # Static views of pure helpers
    # -----------------------------


def _get_whisper_model():
    """Initialize and cache the Whisper model once per process."""
    global _WHISPER_MODEL
    if _WHISPER_MODEL is not None:
        return _WHISPER_MODEL
    with _WHISPER_MODEL_LOCK:
        if _WHISPER_MODEL is None:
            _WHISPER_MODEL = WhisperModel("base.en", device="cpu", compute_type="int8")
    return _WHISPER_MODEL
