<?php

namespace App\Services\Video;

use Illuminate\Support\Facades\Log;

class VideoAssemblyService
{
    /**
     * 영상 클립 결합 (concat)
     *
     * @param  array  $clipPaths  클립 파일 경로 배열
     * @return string|null 결합된 영상 경로
     */
    public function concatClips(array $clipPaths, string $outputPath): ?string
    {
        if (empty($clipPaths)) {
            return null;
        }

        // 클립이 1개면 그대로 복사
        if (count($clipPaths) === 1) {
            copy($clipPaths[0], $outputPath);

            return $outputPath;
        }

        $dir = dirname($outputPath);
        if (! is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        // concat용 파일 리스트 생성
        $listFile = "{$dir}/concat_list.txt";
        $listContent = '';

        foreach ($clipPaths as $path) {
            $listContent .= 'file '.escapeshellarg($path)."\n";
        }

        file_put_contents($listFile, $listContent);

        // 모든 클립을 동일 형식으로 재인코딩 후 concat
        $scaledPaths = [];
        foreach ($clipPaths as $i => $path) {
            $scaledPath = "{$dir}/scaled_{$i}.mp4";
            $scaleCmd = sprintf(
                'ffmpeg -y -i %s -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,setsar=1" -r 30 -c:v libx264 -preset fast -crf 23 -an %s 2>&1',
                escapeshellarg($path),
                escapeshellarg($scaledPath)
            );
            exec($scaleCmd, $output, $returnCode);

            if ($returnCode !== 0) {
                Log::error('VideoAssemblyService: 클립 스케일링 실패', [
                    'clip' => $path,
                    'output' => implode("\n", $output),
                ]);

                return null;
            }

            $scaledPaths[] = $scaledPath;
        }

        // 스케일된 클립 리스트
        $scaledListFile = "{$dir}/scaled_list.txt";
        $scaledListContent = '';
        foreach ($scaledPaths as $path) {
            $scaledListContent .= 'file '.escapeshellarg($path)."\n";
        }
        file_put_contents($scaledListFile, $scaledListContent);

        $cmd = sprintf(
            'ffmpeg -y -f concat -safe 0 -i %s -c copy %s 2>&1',
            escapeshellarg($scaledListFile),
            escapeshellarg($outputPath)
        );

        exec($cmd, $output, $returnCode);

        // 임시 파일 정리
        @unlink($listFile);
        @unlink($scaledListFile);
        foreach ($scaledPaths as $path) {
            @unlink($path);
        }

        if ($returnCode !== 0) {
            Log::error('VideoAssemblyService: 클립 결합 실패', [
                'output' => implode("\n", $output),
            ]);

            return null;
        }

        return $outputPath;
    }

    /**
     * 나레이션 오디오 파일들을 하나로 합치기
     *
     * @param  array  $audioPaths  [scene_number => file_path]
     * @param  array  $scenes  시나리오 장면 정보 (duration 사용)
     */
    public function concatNarrations(array $audioPaths, array $scenes, string $outputPath): ?string
    {
        if (empty($audioPaths)) {
            return null;
        }

        $dir = dirname($outputPath);
        if (! is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        // 각 나레이션을 장면 길이에 정확히 맞춤 (atrim으로 자르고 apad로 채움)
        $paddedPaths = [];
        foreach ($scenes as $scene) {
            $sceneNum = $scene['scene_number'];
            $duration = $scene['duration'] ?? 8;

            if (isset($audioPaths[$sceneNum])) {
                $paddedPath = "{$dir}/narration_padded_{$sceneNum}.mp3";
                // atrim: 장면보다 긴 나레이션은 잘라냄, apad: 짧으면 무음으로 채움
                $cmd = sprintf(
                    'ffmpeg -y -i %s -af "atrim=0:%d,apad=whole_dur=%d" -c:a libmp3lame -q:a 2 %s 2>&1',
                    escapeshellarg($audioPaths[$sceneNum]),
                    $duration,
                    $duration,
                    escapeshellarg($paddedPath)
                );
                exec($cmd, $output, $returnCode);

                if ($returnCode === 0) {
                    $paddedPaths[] = $paddedPath;
                } else {
                    $paddedPaths[] = $audioPaths[$sceneNum];
                }
            }
        }

        if (empty($paddedPaths)) {
            return null;
        }

        // 나레이션 결합
        if (count($paddedPaths) === 1) {
            copy($paddedPaths[0], $outputPath);
        } else {
            $listFile = "{$dir}/narration_list.txt";
            $listContent = '';
            foreach ($paddedPaths as $path) {
                $listContent .= 'file '.escapeshellarg($path)."\n";
            }
            file_put_contents($listFile, $listContent);

            $cmd = sprintf(
                'ffmpeg -y -f concat -safe 0 -i %s -c:a libmp3lame -q:a 2 %s 2>&1',
                escapeshellarg($listFile),
                escapeshellarg($outputPath)
            );

            exec($cmd, $output, $returnCode);
            @unlink($listFile);

            if ($returnCode !== 0) {
                Log::error('VideoAssemblyService: 나레이션 결합 실패', [
                    'output' => implode("\n", $output),
                ]);

                return null;
            }
        }

        // 임시 패딩 파일 정리
        foreach ($paddedPaths as $path) {
            if (str_contains($path, 'narration_padded_')) {
                @unlink($path);
            }
        }

        return $outputPath;
    }

    /**
     * ffprobe로 오디오 파일의 실제 재생 시간 측정
     */
    public function getAudioDuration(string $path): float
    {
        if (! file_exists($path)) {
            return 0.0;
        }

        $cmd = sprintf(
            'ffprobe -v quiet -show_entries format=duration -of csv=p=0 %s 2>/dev/null',
            escapeshellarg($path)
        );

        $output = trim(shell_exec($cmd) ?? '');

        return (float) $output;
    }

    /**
     * ASS 자막 파일 생성 (실제 TTS 오디오 길이 기반 싱크)
     *
     * @param  array  $scenes  [{scene_number, narration, duration}, ...]
     * @param  array  $narrationDurations  [scene_number => 실제 오디오 초] (ffprobe 측정값)
     */
    /**
     * @param  string  $layout  'portrait' (9:16 Shorts) 또는 'landscape' (16:9 튜토리얼)
     */
    public function generateAssSubtitle(array $scenes, string $outputPath, array $narrationDurations = [], string $layout = 'portrait'): string
    {
        $dir = dirname($outputPath);
        if (! is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        $isLandscape = $layout === 'landscape';

        $ass = "[Script Info]\n";
        $ass .= "ScriptType: v4.00+\n";

        if ($isLandscape) {
            // 가로 영상 (16:9 튜토리얼)
            $ass .= "PlayResX: 1920\n";
            $ass .= "PlayResY: 1080\n";
        } else {
            // 세로 영상 (9:16 Shorts)
            $ass .= "PlayResX: 1080\n";
            $ass .= "PlayResY: 1920\n";
        }
        $ass .= "WrapStyle: 2\n\n"; // 2 = no word wrapping (한줄 유지)

        $ass .= "[V4+ Styles]\n";
        $ass .= "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n";

        if ($isLandscape) {
            // 가로 영상: 하단 중앙, 폰트 작게, 여백 넓게, 한줄 최대
            // Alignment=2: 하단 중앙
            // MarginL=20, MarginR=20: 좌우 여백 최소화 → 길게
            // MarginV=30: 하단에서 약간 위
            // FontSize=48: 1920 기준 적절한 크기
            // BorderStyle=3: 반투명 배경 박스
            // BackColour=&H80000000: 반투명 검정 배경
            $ass .= "Style: Default,NanumGothic,48,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,3,2,0,2,20,20,30,1\n\n";
        } else {
            // 세로 영상 (기존)
            $ass .= "Style: Default,NanumGothic,96,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,5,2,5,40,40,0,1\n\n";
        }

        $ass .= "[Events]\n";
        $ass .= "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n";

        $currentTime = 0;
        // 가로 영상은 줄바꿈 없이 한줄, 세로 영상은 16자 줄바꿈
        $maxCharsPerLine = $isLandscape ? 999 : 16;

        foreach ($scenes as $scene) {
            $sceneDuration = $scene['duration'] ?? 8;
            $sceneNum = (int) ($scene['scene_number'] ?? 0);
            $narration = $scene['narration'] ?? '';

            // 인트로(1)/아웃트로(999) 씬은 슬라이드에 텍스트가 포함 → 자막 생략
            // 또한 scene_number < 100인 경우도 인트로로 간주 (안전장치)
            $isIntroOutro = $sceneNum <= 1 || $sceneNum === 999;

            if (empty($narration) || $isIntroOutro) {
                $currentTime += $sceneDuration;

                continue;
            }

            // 이모지/특수문자 제거 후 문장 분리
            $cleanNarration = $this->stripEmoji($narration);
            $sentences = $this->splitIntoSentences($cleanNarration);

            // 실제 TTS 오디오 길이 사용 (없으면 장면 길이의 90%로 추정)
            $audioDuration = $narrationDurations[$sceneNum] ?? ($sceneDuration * 0.9);
            // 장면 길이를 초과하지 않도록 제한
            $activeTime = min($audioDuration, $sceneDuration);

            $totalChars = array_sum(array_map('mb_strlen', $sentences));
            $offset = 0.0;

            foreach ($sentences as $sentence) {
                $sentence = trim($sentence);
                if (empty($sentence)) {
                    continue;
                }

                $ratio = mb_strlen($sentence) / max($totalChars, 1);
                $sentDuration = max($activeTime * $ratio, 1.0);

                // 장면 경계 초과 방지
                if ($offset + $sentDuration > $sceneDuration) {
                    $sentDuration = $sceneDuration - $offset;
                }
                if ($sentDuration <= 0) {
                    break;
                }

                $startTime = $this->formatAssTime($currentTime + $offset);
                $endTime = $this->formatAssTime($currentTime + $offset + $sentDuration);

                $text = $this->wrapText($sentence, $maxCharsPerLine);
                $text = str_replace("\n", '\\N', $text);

                $ass .= "Dialogue: 0,{$startTime},{$endTime},Default,,0,0,0,,{$text}\n";

                $offset += $sentDuration;
            }

            $currentTime += $sceneDuration;
        }

        file_put_contents($outputPath, $ass);

        return $outputPath;
    }

    /**
     * 자막용 이모지/특수문자 제거
     */
    private function stripEmoji(string $text): string
    {
        $text = preg_replace('/[\x{1F600}-\x{1F64F}]/u', '', $text);
        $text = preg_replace('/[\x{1F300}-\x{1F5FF}]/u', '', $text);
        $text = preg_replace('/[\x{1F680}-\x{1F6FF}]/u', '', $text);
        $text = preg_replace('/[\x{1F900}-\x{1F9FF}]/u', '', $text);
        $text = preg_replace('/[\x{1FA00}-\x{1FAFF}]/u', '', $text);
        $text = preg_replace('/[\x{2600}-\x{27BF}]/u', '', $text);
        $text = preg_replace('/[\x{FE00}-\x{FE0F}]/u', '', $text);
        $text = preg_replace('/[\x{200D}]/u', '', $text);
        $text = preg_replace('/[\x{E0020}-\x{E007F}]/u', '', $text);
        $text = str_replace('*', '', $text);
        $text = preg_replace('/\([^)]*\)/', '', $text);
        $text = preg_replace('/\[[^\]]*\]/', '', $text);
        $text = preg_replace('/[○●◎◇◆□■△▲▽▼★☆♡♥]/u', '', $text);
        $text = preg_replace('/\s+/', ' ', $text);

        return trim($text);
    }

    /**
     * 나레이션 텍스트를 문장 단위로 분리
     *
     * 규칙:
     * - 구두점(연속 포함) + 공백 기준으로 분리 ("?!" 등은 하나로 유지)
     * - 5자 미만 조각은 이전 문장에 병합 (구두점이 다음 페이지로 넘어가는 버그 방지)
     */
    private function splitIntoSentences(string $text): array
    {
        $text = trim($text);

        // 전체가 20자 이하면 분리하지 않음 (한 문장)
        if (mb_strlen($text) <= 20) {
            return [$text];
        }

        // \K로 구두점은 앞 문장에 유지, 공백만 구분자로 소비
        // "정말요?!" → 분리 안 됨, "정말요?! 다음 문장" → ["정말요?!", "다음 문장"]
        $rawParts = preg_split('/[.!?。！？](?![.!?。！？])\K\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);

        if (count($rawParts) <= 1) {
            return [$text];
        }

        // 5자 미만 짧은 조각은 이전 문장에 병합 (backward merge)
        $merged = [];

        foreach ($rawParts as $part) {
            $part = trim($part);
            if ($part === '') {
                continue;
            }

            if (mb_strlen($part) < 5 && ! empty($merged)) {
                // 짧은 조각 → 이전 항목 뒤에 붙임
                $merged[count($merged) - 1] .= ' '.$part;
            } else {
                $merged[] = $part;
            }
        }

        return ! empty($merged) ? $merged : [$text];
    }

    /**
     * 최종 합성: 영상 + 나레이션 + BGM + 자막
     */
    public function assemble(
        string $videoPath,
        ?string $narrationPath,
        ?string $bgmPath,
        ?string $subtitlePath,
        string $outputPath
    ): ?string {
        $dir = dirname($outputPath);
        if (! is_dir($dir)) {
            mkdir($dir, 0755, true);
        }

        $inputs = ['-i '.escapeshellarg($videoPath)];
        $filterParts = [];
        $mapParts = ['-map 0:v'];
        $audioIndex = 1;

        // 나레이션 추가
        if ($narrationPath && file_exists($narrationPath)) {
            $inputs[] = '-i '.escapeshellarg($narrationPath);
            $filterParts[] = "[{$audioIndex}:a]volume=2.0[nar]";
            $audioIndex++;
        }

        // BGM 추가
        if ($bgmPath && file_exists($bgmPath)) {
            $inputs[] = '-i '.escapeshellarg($bgmPath);
            $filterParts[] = "[{$audioIndex}:a]volume=1.2[bgm]";
            $audioIndex++;
        }

        // 오디오 믹싱 필터
        if (count($filterParts) === 2) {
            // 나레이션 + BGM
            $filterComplex = implode(';', $filterParts).';[nar][bgm]amix=inputs=2:duration=first[a]';
            $mapParts[] = '-map "[a]"';
        } elseif (count($filterParts) === 1) {
            // 나레이션 또는 BGM 중 하나만
            $streamName = $narrationPath ? 'nar' : 'bgm';
            $filterComplex = $filterParts[0];
            $mapParts[] = '-map "['.$streamName.']"';
        } else {
            $filterComplex = null;
        }

        // FFmpeg 명령 조립
        $cmd = 'ffmpeg -y '.implode(' ', $inputs);

        if ($filterComplex) {
            $cmd .= ' -filter_complex "'.$filterComplex.'"';
        }

        $cmd .= ' '.implode(' ', $mapParts);

        // 자막 비디오 필터 (슬라이드 캡션바에 텍스트 포함 시 생략 가능)
        if ($subtitlePath && file_exists($subtitlePath)) {
            $vf = sprintf('ass=%s', escapeshellarg($subtitlePath));
            $cmd .= ' -vf '.escapeshellarg($vf);
        }
        $cmd .= ' -c:v libx264 -preset fast -crf 23 -r 30';
        $cmd .= ' -c:a aac -b:a 192k';
        $cmd .= ' -shortest';
        $cmd .= ' '.escapeshellarg($outputPath);
        $cmd .= ' 2>&1';

        Log::info('VideoAssemblyService: 최종 합성 시작', ['cmd' => $cmd]);

        exec($cmd, $output, $returnCode);

        if ($returnCode !== 0) {
            Log::error('VideoAssemblyService: 최종 합성 실패', [
                'return_code' => $returnCode,
                'output' => implode("\n", array_slice($output, -20)),
            ]);

            return null;
        }

        Log::info('VideoAssemblyService: 최종 합성 완료', [
            'output' => $outputPath,
            'size' => file_exists($outputPath) ? filesize($outputPath) : 0,
        ]);

        return $outputPath;
    }

    /**
     * ASS 시간 형식 (H:MM:SS.cs)
     */
    private function formatAssTime(float $seconds): string
    {
        $hours = floor($seconds / 3600);
        $minutes = floor(($seconds % 3600) / 60);
        $secs = floor($seconds % 60);
        $centiseconds = round(($seconds - floor($seconds)) * 100);

        return sprintf('%d:%02d:%02d.%02d', $hours, $minutes, $secs, $centiseconds);
    }

    /**
     * 긴 텍스트를 일정 글자수로 줄바꿈
     */
    private function wrapText(string $text, int $maxCharsPerLine): string
    {
        if (mb_strlen($text) <= $maxCharsPerLine) {
            return $text;
        }

        $lines = [];
        $words = preg_split('/\s+/', $text);
        $currentLine = '';

        foreach ($words as $word) {
            if (mb_strlen($currentLine.' '.$word) > $maxCharsPerLine && $currentLine !== '') {
                $lines[] = trim($currentLine);
                $currentLine = $word;
            } else {
                $currentLine .= ($currentLine ? ' ' : '').$word;
            }
        }

        if ($currentLine !== '') {
            $lines[] = trim($currentLine);
        }

        return implode("\n", $lines);
    }

    /**
     * 작업 디렉토리 정리
     */
    public function cleanup(string $workDir): void
    {
        if (! is_dir($workDir)) {
            return;
        }

        $files = glob("{$workDir}/*");
        foreach ($files as $file) {
            if (is_file($file) && ! str_contains($file, 'final_')) {
                @unlink($file);
            }
        }
    }
}
