"""
视频素材分析器 - 基于 FFmpeg + NumPy 的轻量级智能分析

提供：
1. 素材分类 - 基于视频特征的多维度分析
2. 质量评分 - 基于分辨率、帧率、码率、清晰度、稳定性的综合评分
"""

from __future__ import annotations

import json
import logging
import math
import os
import subprocess
import tempfile
from dataclasses import dataclass, field
from typing import Any

import numpy as np
from PIL import Image

from packages.domain.classification import AssetClassification

logger = logging.getLogger(__name__)


@dataclass
class VideoInfo:
    """视频基本信息"""

    width: int = 0
    height: int = 0
    fps: float = 0.0
    duration: float = 0.0
    bitrate: int = 0
    codec: str = ""
    has_audio: bool = False
    file_size: int = 0


@dataclass
class ColorAnalysis:
    """色彩分析结果"""

    dominant_hue: float = 0.0  # 主色调 (0-360)
    green_ratio: float = 0.0  # 绿色占比
    warm_ratio: float = 0.0  # 暖色调占比
    cool_ratio: float = 0.0  # 冷色调占比
    avg_saturation: float = 0.0
    avg_brightness: float = 0.0


@dataclass
class MotionAnalysis:
    """运动分析结果"""

    motion_score: float = 0.0  # 运动幅度 (0-1)
    scene_changes: int = 0  # 场景切换次数


@dataclass
class AudioAnalysis:
    """音频分析结果"""

    has_audio: bool = False
    speech_ratio: float = 0.0  # 人声比例
    music_ratio: float = 0.0  # 音乐比例
    ambient_ratio: float = 0.0  # 环境音比例


@dataclass
class ClassificationResult:
    """分类结果"""

    category: AssetClassification
    confidence: float
    scores: dict[str, float] = field(default_factory=dict)


@dataclass
class QualityScore:
    """质量评分结果"""

    total: float
    resolution_score: float = 0.0
    fps_score: float = 0.0
    bitrate_score: float = 0.0
    clarity_score: float = 0.0
    stability_score: float = 0.0


class AssetAnalyzer:
    """
    轻量级视频素材分析器

    使用 FFmpeg + NumPy 进行视频特征分析，不依赖外部 AI API。
    """

    def __init__(self, video_path: str, temp_dir: str | None = None):
        """
        初始化分析器

        Args:
            video_path: 视频文件路径
            temp_dir: 临时目录，用于存储提取的帧
        """
        self.video_path = video_path
        self._video_info: VideoInfo | None = None
        self._frames: list[np.ndarray] | None = None
        self._temp_dir = temp_dir or tempfile.mkdtemp(prefix="asset_analyzer_")

    def __del__(self):
        """清理临时文件"""
        self._cleanup_temp_dir()

    def _cleanup_temp_dir(self):
        """清理临时目录"""
        try:
            import shutil

            if os.path.exists(self._temp_dir):
                shutil.rmtree(self._temp_dir)
        except Exception as e:
            logger.warning(f"Operation failed in apps/worker/worker_app/tasks/asset_analyzer.py: {e}", exc_info=True)

    def get_video_info(self) -> VideoInfo:
        """获取视频基本信息"""
        if self._video_info is not None:
            return self._video_info

        info = VideoInfo()

        try:
            cmd = [
                "ffprobe",
                "-v",
                "quiet",
                "-print_format",
                "json",
                "-show_format",
                "-show_streams",
                self.video_path,
            ]
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=30,
            )

            if result.returncode == 0:
                data = json.loads(result.stdout)
                streams = data.get("streams", [])
                format_info = data.get("format", {})

                for stream in streams:
                    if stream.get("codec_type") == "video":
                        info.width = int(stream.get("width", 0))
                        info.height = int(stream.get("height", 0))
                        info.codec = stream.get("codec_name", "")

                        # 解析帧率
                        fps_str = stream.get("r_frame_rate", "0/1")
                        if "/" in fps_str:
                            num, denom = fps_str.split("/")
                            info.fps = float(num) / float(denom) if float(denom) != 0 else 0.0
                        else:
                            info.fps = float(fps_str)

                    elif stream.get("codec_type") == "audio":
                        info.has_audio = True

                info.duration = float(format_info.get("duration", 0))
                info.bitrate = int(format_info.get("bit_rate", 0))
                info.file_size = int(format_info.get("size", 0))

        except Exception as e:
            logger.warning(f"Failed to get video info: {e}")

        self._video_info = info
        return info

    def extract_frames(self, count: int = 10, max_frames: int = 30) -> list[np.ndarray]:
        """
        从视频中均匀抽取帧

        Args:
            count: 抽取的帧数

        Returns:
            帧数据列表 (RGB 格式)
        """
        if self._frames is not None:
            return self._frames

        frames = []
        info = self.get_video_info()

        if info.duration <= 0:
            logger.warning("Video duration is 0, cannot extract frames")
            return frames

        try:
            # 计算采样间隔
            interval = max(1.0, info.duration / count)

            for i in range(count):
                timestamp = i * interval

                # 提取单帧为 PNG
                output_path = os.path.join(self._temp_dir, f"frame_{i:03d}.png")
                cmd = [
                    "ffmpeg",
                    "-y",  # 覆盖输出文件
                    "-ss",
                    str(timestamp),
                    "-i",
                    self.video_path,
                    "-vframes",
                    "1",
                    "-q:v",
                    "2",  # 高质量
                    "-f",
                    "image2",
                    output_path,
                ]

                result = subprocess.run(
                    cmd,
                    capture_output=True,
                    text=True,
                    timeout=10,
                )

                if result.returncode == 0 and os.path.exists(output_path):
                    # 读取帧并转换为 numpy 数组
                    img = self._load_image_as_array(output_path)
                    if img is not None:
                        frames.append(img)

        except Exception as e:
            logger.warning(f"Failed to extract frames: {e}")

        self._frames = frames
        return frames

    def _load_image_as_array(self, path: str) -> np.ndarray | None:
        """加载图片为 numpy 数组 (RGB 格式)"""
        try:
            from PIL import Image

            with Image.open(path) as img:
                if img.mode != "RGB":
                    img = img.convert("RGB")
                return np.array(img)
        except Exception as e:
            logger.warning(f"Failed to load image {path}: {e}")
            return None

    def analyze_color_distribution(self, frames: list[np.ndarray] | None = None) -> ColorAnalysis:
        """
        分析色彩分布 (HSV 空间)

        Returns:
            ColorAnalysis 对象
        """
        if frames is None:
            frames = self.extract_frames()

        if not frames:
            return ColorAnalysis()

        result = ColorAnalysis()
        all_hsv = []

        try:
            for frame in frames:
                # RGB 转 HSV
                rgb = frame.astype(float) / 255.0
                r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2]

                maxc = np.maximum(np.maximum(r, g), b)
                minc = np.minimum(np.minimum(r, g), b)
                v = maxc
                s = np.where(maxc > 0, (maxc - minc) / maxc, 0)

                # 计算色相
                rc = np.where(maxc == r, (maxc - g - (maxc - b)) / (maxc - minc + 1e-10), 0)
                gc = np.where(maxc == g, 2.0 + (maxc - b - (maxc - r)) / (maxc - minc + 1e-10), 0)
                bc = np.where(maxc == b, 4.0 + (maxc - r - (maxc - g)) / (maxc - minc + 1e-10), 0)
                h = (rc + gc + bc) * 60
                h = np.where(h < 0, h + 360, h)

                frame_hsv = np.stack([h.flatten(), s.flatten(), v.flatten()], axis=1)
                all_hsv.append(frame_hsv)

            if all_hsv:
                all_hsv = np.vstack(all_hsv)

                # 主色调
                result.dominant_hue = float(np.median(all_hsv[:, 0]))

                # 饱和度
                result.avg_saturation = float(np.mean(all_hsv[:, 1]))

                # 亮度
                result.avg_brightness = float(np.mean(all_hsv[:, 2]))

                # 计算颜色比例
                # 绿色: 60-180 度
                green_mask = (all_hsv[:, 0] >= 60) & (all_hsv[:, 0] <= 180)
                result.green_ratio = float(np.mean(green_mask))

                # 暖色调 (红/黄/橙): 0-60, 300-360 度
                warm_mask = (all_hsv[:, 0] <= 60) | (all_hsv[:, 0] >= 300)
                result.warm_ratio = float(np.mean(warm_mask))

                # 冷色调 (蓝/青): 180-300 度
                cool_mask = (all_hsv[:, 0] >= 180) & (all_hsv[:, 0] <= 300)
                result.cool_ratio = float(np.mean(cool_mask))

        except Exception as e:
            logger.warning(f"Failed to analyze color distribution: {e}")

        return result

    def analyze_motion(self, frames: list[np.ndarray] | None = None) -> MotionAnalysis:
        """
        分析画面运动幅度

        Returns:
            MotionAnalysis 对象
        """
        if frames is None:
            frames = self.extract_frames()

        if len(frames) < 2:
            return MotionAnalysis()

        result = MotionAnalysis()
        motion_scores = []
        scene_changes = 0

        try:
            for i in range(len(frames) - 1):
                # 计算相邻帧差异
                diff = np.abs(frames[i + 1].astype(float) - frames[i].astype(float))
                mean_diff = np.mean(diff) / 255.0

                motion_scores.append(mean_diff)

                # 检测场景切换 (帧差异 > 30%)
                if mean_diff > 0.3:
                    scene_changes += 1

            if motion_scores:
                # 使用中位数避免异常值影响
                result.motion_score = float(np.median(motion_scores))
                # 归一化到 0-1
                result.motion_score = min(1.0, result.motion_score * 5)
                result.scene_changes = scene_changes

        except Exception as e:
            logger.warning(f"Failed to analyze motion: {e}")

        return result

    def analyze_audio(self) -> AudioAnalysis:
        """
        分析音频特征

        Returns:
            AudioAnalysis 对象
        """
        result = AudioAnalysis()
        info = self.get_video_info()

        if not info.has_audio:
            return result

        result.has_audio = True

        try:
            # 提取音频并分析频率特征
            audio_path = os.path.join(self._temp_dir, "audio.wav")
            cmd = [
                "ffmpeg",
                "-y",
                "-i",
                self.video_path,
                "-vn",  # 不要视频
                "-ac",
                "1",  # 单声道
                "-ar",
                "8000",  # 降低采样率
                "-f",
                "wav",
                audio_path,
            ]

            result_audio = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=30,
            )

            if result_audio.returncode == 0 and os.path.exists(audio_path):
                # 读取音频数据
                import struct

                with open(audio_path, "rb") as f:
                    # 跳过 WAV 头
                    f.read(44)
                    audio_data = f.read()

                if len(audio_data) >= 2:
                    # 转换为 numpy 数组
                    audio_samples = np.array(struct.unpack(f"<{len(audio_data)//2}h", audio_data), dtype=float)
                    audio_samples = audio_samples / 32768.0

                    if len(audio_samples) > 0:
                        # 简单频谱分析
                        fft = np.abs(np.fft.rfft(audio_samples[: min(len(audio_samples), 8000)]))
                        freqs = np.fft.rfftfreq(min(len(audio_samples), 8000), 1 / 8000)

                        # 人声频率: 300-3400 Hz
                        speech_mask = (freqs >= 300) & (freqs <= 3400)
                        speech_energy = np.mean(fft[speech_mask]) if speech_mask.any() else 0

                        # 音乐低频: 60-250 Hz
                        bass_mask = (freqs >= 60) & (freqs <= 250)
                        bass_energy = np.mean(fft[bass_mask]) if bass_mask.any() else 0

                        # 环境音 (高频): > 4000 Hz
                        high_mask = freqs > 4000
                        high_energy = np.mean(fft[high_mask]) if high_mask.any() else 0

                        total_energy = speech_energy + bass_energy + high_energy + 1e-10

                        result.speech_ratio = float(speech_energy / total_energy)
                        result.music_ratio = float(bass_energy / total_energy)
                        result.ambient_ratio = float(high_energy / total_energy)

        except Exception as e:
            logger.warning(f"Failed to analyze audio: {e}")

        return result

    def classify(self) -> ClassificationResult:
        """
        综合分析得出分类结果

        Returns:
            ClassificationResult 对象
        """
        # 提取分析数据
        frames = self.extract_frames()
        color = self.analyze_color_distribution(frames)
        motion = self.analyze_motion(frames)
        audio = self.analyze_audio()

        # 计算各类别得分
        scores = self._calculate_category_scores(color, motion, audio)

        # 找最高分
        if not scores:
            return ClassificationResult(
                category=AssetClassification.OTHER,
                confidence=0.3,
                scores={},
            )

        best_category = max(scores.items(), key=lambda x: x[1])
        category = AssetClassification(best_category[0])
        confidence = min(0.95, max(0.3, best_category[1]))

        return ClassificationResult(
            category=category,
            confidence=confidence,
            scores=scores,
        )

    def _calculate_category_scores(
        self,
        color: ColorAnalysis,
        motion: MotionAnalysis,
        audio: AudioAnalysis,
    ) -> dict[str, float]:
        """
        计算各类别的置信度得分

        Args:
            color: 色彩分析结果
            motion: 运动分析结果
            audio: 音频分析结果

        Returns:
            各类别得分字典
        """
        scores = {}

        # 1. 风景 (scenic) - 绿色、户外、自然
        scenic_score = 0.0
        if color.green_ratio > 0.3:
            scenic_score += 0.4 * color.green_ratio
        if color.avg_saturation > 0.3:
            scenic_score += 0.2 * color.avg_saturation
        if color.avg_brightness > 0.4:
            scenic_score += 0.2
        if motion.motion_score > 0.1 and motion.motion_score < 0.5:
            scenic_score += 0.2  # 适度运动（如云朵、树叶）
        if not audio.has_audio or audio.ambient_ratio > 0.5:
            scenic_score += 0.2  # 自然环境音
        scores[AssetClassification.SCENIC.value] = min(1.0, scenic_score)

        # 2. 产品 (product) - 中等亮度、均匀色彩、低运动
        product_score = 0.0
        if 0.3 < color.avg_brightness < 0.7:
            product_score += 0.3
        if color.avg_saturation < 0.5:
            product_score += 0.2
        if motion.motion_score < 0.15:
            product_score += 0.4  # 低运动 = 产品展示
        if color.cool_ratio > 0.3:
            product_score += 0.2  # 冷色调 = 科技感
        scores[AssetClassification.PRODUCT.value] = min(1.0, product_score)

        # 3. 人物 (person) - 中等运动、有时有人声
        person_score = 0.0
        if 0.1 < motion.motion_score < 0.4:
            person_score += 0.3  # 适度运动
        if audio.has_audio and audio.speech_ratio > 0.3:
            person_score += 0.5  # 有人声
        if color.avg_brightness > 0.3:
            person_score += 0.2
        scores[AssetClassification.PERSON.value] = min(1.0, person_score)

        # 4. 动物 (animal) - 高运动、有时自然音
        animal_score = 0.0
        if motion.motion_score > 0.3:
            animal_score += 0.4  # 高运动
        if motion.scene_changes > 2:
            animal_score += 0.2
        if audio.has_audio and (audio.ambient_ratio > 0.3 or audio.speech_ratio > 0.2):
            animal_score += 0.3
        scores[AssetClassification.ANIMAL.value] = min(1.0, animal_score)

        # 5. 美食 (food) - 暖色调、高饱和度
        food_score = 0.0
        if color.warm_ratio > 0.4:
            food_score += 0.5
        if color.avg_saturation > 0.5:
            food_score += 0.3
        if 0.4 < color.avg_brightness < 0.8:
            food_score += 0.2
        scores[AssetClassification.FOOD.value] = min(1.0, food_score)

        # 6. 科技 (tech) - 冷色调、低饱和度、低运动
        tech_score = 0.0
        if color.cool_ratio > 0.4:
            tech_score += 0.4
        if color.avg_saturation < 0.4:
            tech_score += 0.3
        if motion.motion_score < 0.2:
            tech_score += 0.3
        scores[AssetClassification.TECH.value] = min(1.0, tech_score)

        # 7. 运动 (sport) - 高运动
        sport_score = 0.0
        if motion.motion_score > 0.4:
            sport_score += 0.6
        if motion.scene_changes > 3:
            sport_score += 0.2
        if color.avg_brightness > 0.4:
            sport_score += 0.2
        scores[AssetClassification.SPORT.value] = min(1.0, sport_score)

        # 8. 音乐 (music) - 有节奏性音乐
        music_score = 0.0
        if audio.has_audio and audio.music_ratio > 0.4:
            music_score += 0.6
        # 纯视觉判断：色彩丰富但非自然
        if color.avg_saturation > 0.5 and color.green_ratio < 0.2:
            music_score += 0.3
        scores[AssetClassification.MUSIC.value] = min(1.0, music_score)

        # 9. 其他 (other) - 默认最低分
        scores[AssetClassification.OTHER.value] = 0.1

        return scores

    def calculate_quality_score(self) -> QualityScore:
        """
        计算视频质量综合评分 (0-100)

        评分维度:
        1. 分辨率得分 (25分)
        2. 帧率得分 (20分)
        3. 码率得分 (20分)
        4. 清晰度得分 (20分) - Laplacian 方差
        5. 稳定性得分 (15分) - 帧间位移方差
        """
        info = self.get_video_info()
        frames = self.extract_frames()

        # 1. 分辨率得分
        resolution_score = self._score_resolution(info.width, info.height)

        # 2. 帧率得分
        fps_score = self._score_framerate(info.fps)

        # 3. 码率得分
        bitrate_score = self._score_bitrate(info.bitrate)

        # 4. 清晰度得分
        clarity_score = self._score_clarity(frames)

        # 5. 稳定性得分
        stability_score = self._score_stability(frames)

        total = resolution_score + fps_score + bitrate_score + clarity_score + stability_score

        return QualityScore(
            total=round(min(100, max(0, total)), 1),
            resolution_score=resolution_score,
            fps_score=fps_score,
            bitrate_score=bitrate_score,
            clarity_score=clarity_score,
            stability_score=stability_score,
        )

    def _score_resolution(self, width: int, height: int) -> float:
        """分辨率评分 (满分 25)"""
        pixels = width * height

        if pixels >= 3840 * 2160:  # 4K
            return 25.0
        elif pixels >= 2560 * 1440:  # 2K
            return 22.0
        elif pixels >= 1920 * 1080:  # 1080p
            return 20.0
        elif pixels >= 1280 * 720:  # 720p
            return 15.0
        elif pixels >= 854 * 480:  # 480p
            return 8.0
        else:
            return 3.0

    def _score_framerate(self, fps: float) -> float:
        """帧率评分 (满分 20)"""
        if fps >= 60:
            return 20.0
        elif fps >= 30:
            return 15.0
        elif fps >= 24:
            return 10.0
        elif fps >= 15:
            return 7.0
        else:
            return 5.0

    def _score_bitrate(self, bitrate: int) -> float:
        """码率评分 (满分 20)"""
        bitrate_mbps = bitrate / 1_000_000

        if bitrate_mbps > 10:
            return 20.0
        elif bitrate_mbps >= 5:
            return 15.0
        elif bitrate_mbps >= 2:
            return 10.0
        elif bitrate_mbps >= 0.5:
            return 5.0
        else:
            return 3.0

    def _score_clarity(self, frames: list[np.ndarray]) -> float:
        """
        清晰度评分 (满分 20)

        使用 Laplacian 方差评估画面清晰度
        高方差 = 细节丰富 = 高分
        """
        if not frames:
            return 10.0  # 默认中等分

        try:
            variances = []

            for frame in frames[:5]:  # 只分析前 5 帧
                if len(frame.shape) == 3:
                    # 转灰度
                    gray = np.dot(frame[..., :3], [0.299, 0.587, 0.114]).astype(np.uint8)
                else:
                    gray = frame

                # Laplacian 算子
                laplacian = np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]], dtype=np.float32)

                # 手动计算卷积
                from scipy import signal

                laplacian_img = signal.convolve2d(gray.astype(float), laplacian, mode="same")
                variance = np.var(laplacian_img)
                variances.append(variance)

            # 归一化方差到 0-20 分
            avg_variance = np.mean(variances)
            # 根据经验值调整
            score = min(20.0, avg_variance / 100)
            return float(score)

        except ImportError:
            # 如果没有 scipy，使用简化方法
            return 10.0
        except Exception:
            return 10.0

    def _score_stability(self, frames: list[np.ndarray]) -> float:
        """
        稳定性评分 (满分 15)

        分析帧间位移方差
        画面稳定 = 高分
        剧烈抖动 = 低分
        """
        if len(frames) < 2:
            return 10.0  # 默认中等分

        try:
            displacements = []

            for i in range(len(frames) - 1):
                # 缩小帧以加速处理
                scale = 0.25
                new_h = int(frames[i].shape[0] * scale)
                new_w = int(frames[i].shape[1] * scale)
                frame1_small = np.array(Image.fromarray(frames[i]).resize((new_w, new_h)))
                new_h2 = int(frames[i + 1].shape[0] * scale)
                new_w2 = int(frames[i + 1].shape[1] * scale)
                frame2_small = np.array(Image.fromarray(frames[i + 1]).resize((new_w2, new_h2)))

                # 简单位移检测：灰度差
                gray1 = np.mean(frame1_small, axis=2) if len(frame1_small.shape) == 3 else frame1_small
                gray2 = np.mean(frame2_small, axis=2) if len(frame2_small.shape) == 3 else frame2_small

                diff = np.abs(gray2.astype(float) - gray1.astype(float))
                displacement = np.mean(diff) / 255.0
                displacements.append(displacement)

            # 高位移方差 = 不稳定
            if displacements:
                displacement_variance = np.var(displacements)
                # 归一化
                instability = min(1.0, displacement_variance * 10)
                score = 15.0 * (1.0 - instability)
                return float(max(0.0, score))

            return 10.0

        except Exception:
            return 10.0


def classify_asset_real(video_path: str) -> tuple[str, float]:
    """
    真实分类入口函数

    Args:
        video_path: 视频文件路径

    Returns:
        (分类类别, 置信度)
    """
    try:
        analyzer = AssetAnalyzer(video_path)
        result = analyzer.classify()
        return result.category.value, result.confidence
    except Exception as e:
        logger.warning(f"Classification failed, using fallback: {e}")
        return AssetClassification.OTHER.value, 0.3


def calculate_quality_score_real(video_path: str) -> float:
    """
    质量评分入口函数

    Args:
        video_path: 视频文件路径

    Returns:
        质量评分 (0-100)
    """
    try:
        analyzer = AssetAnalyzer(video_path)
        result = analyzer.calculate_quality_score()
        return result.total
    except Exception as e:
        logger.warning(f"Quality scoring failed, using fallback: {e}")
        return 50.0
