using System.Globalization;
using System.Text;
using Microsoft.Extensions.Options;
using TeleWave.Application.Common.Interfaces;
using static TeleWave.Infrastructure.Media.FfmpegText;

namespace TeleWave.Infrastructure.Media;

/// <summary>
/// Синтезирует ТВ-заставку перехода полностью на ffmpeg (без исходного файла) по
/// <see cref="BumperRenderSpec"/>: анимированный градиентный фон + текст «Сейчас/Далее» + короткий
/// джингл, и режет результат на те же HLS-сегменты, что и обычный ассет. Длительность фиксированная и
/// кратная сегменту, поэтому эфирная математика не отличает заставку от программы.
/// </summary>
public sealed class FfmpegBumperRenderer(
    MediaPathResolver paths,
    IOptions<StorageOptions> storageOptions,
    IOptions<MediaOptions> mediaOptions
) : IBumperRenderer
{
    private readonly StorageOptions _storage = storageOptions.Value;
    private readonly MediaOptions _media = mediaOptions.Value;

    /// <summary>
    /// Файлы с динамическим текстом заставки. Всё пользователь-редактируемое (названия шоу, подписи,
    /// свободные строки) ffmpeg читает через <c>textfile=</c> с <c>expansion=none</c>: иначе запятая,
    /// <c>;</c>, <c>[</c> или <c>]</c> в тексте ломают (или инъектируют звенья в) цепочку
    /// <c>-filter_complex</c>.
    /// </summary>
    private sealed record TextFiles(string Now, string Next, string NowLabel, string NextLabel)
    {
        public static TextFiles In(string assetDir) =>
            new(
                Path.Combine(assetDir, "now.txt"),
                Path.Combine(assetDir, "next.txt"),
                Path.Combine(assetDir, "nowlabel.txt"),
                Path.Combine(assetDir, "nextlabel.txt")
            );
    }

    public async Task<BumperRenderResult> RenderAsync(
        Guid assetId,
        BumperRenderSpec spec,
        CancellationToken cancellationToken
    )
    {
        var seg = Math.Max(1, _storage.SegmentSeconds);
        // Длительность — вверх до кратности сегменту (оркестратор обычно уже выровнял, страхуемся).
        var target = (int)(Math.Ceiling((double)Math.Max(seg, spec.DurationSeconds) / seg) * seg);

        var assetDir = paths.AssetDir(assetId);
        if (Directory.Exists(assetDir))
            Directory.Delete(assetDir, recursive: true);
        Directory.CreateDirectory(assetDir);

        // Названия шоу / свободные строки.
        var text = TextFiles.In(assetDir);
        var line1 = spec.FreeText ? spec.FreeLine1 : spec.NowTitle;
        var line2 = spec.FreeText ? spec.FreeLine2 : spec.NextTitle;
        await File.WriteAllTextAsync(text.Now, line1, new UTF8Encoding(false), cancellationToken);
        await File.WriteAllTextAsync(text.Next, line2, new UTF8Encoding(false), cancellationToken);

        // Подписи «Сейчас/Далее» нужны лишь в одноимённом режиме — в FreeText их не рисуют.
        if (!spec.FreeText)
        {
            await File.WriteAllTextAsync(
                text.NowLabel,
                spec.NowLabel,
                new UTF8Encoding(false),
                cancellationToken
            );
            await File.WriteAllTextAsync(
                text.NextLabel,
                spec.NextLabel,
                new UTF8Encoding(false),
                cancellationToken
            );
        }

        try
        {
            var args = BuildArgs(assetDir, seg, target, text, spec);
            var result = await ProcessRunner.RunAsync(
                _media.FfmpegPath,
                args,
                lowPriority: true,
                TimeSpan.FromSeconds(_media.TranscodeTimeoutSeconds),
                cancellationToken
            );
            if (result.ExitCode != 0)
                throw new InvalidOperationException(
                    $"ffmpeg (заставка) завершился с кодом {result.ExitCode}: {Tail(result.StdErr)}"
                );

            var playlist = Path.Combine(assetDir, "index.m3u8");
            if (!File.Exists(playlist))
                throw new InvalidOperationException(
                    "ffmpeg не создал плейлист заставки index.m3u8."
                );

            var segmentCount = Directory.GetFiles(assetDir, "seg*.ts").Length;
            if (segmentCount == 0)
                throw new InvalidOperationException(
                    "ffmpeg не создал ни одного сегмента заставки."
                );

            return new BumperRenderResult(
                TimeSpan.FromSeconds(target),
                seg,
                segmentCount,
                spec.Width,
                spec.Height,
                MediaPathResolver.AssetRelativePath(assetId)
            );
        }
        finally
        {
            TryDelete(text.Now);
            TryDelete(text.Next);
            TryDelete(text.NowLabel);
            TryDelete(text.NextLabel);
        }
    }

    private List<string> BuildArgs(
        string assetDir,
        int seg,
        int target,
        TextFiles text,
        BumperRenderSpec spec
    )
    {
        var w = spec.Width;
        var h = spec.Height;

        var titleSize = Math.Max(24, h / 10);
        var labelSize = Math.Max(14, h / 22);
        var gap = (int)(labelSize * 1.4);

        // Доступная ширина под текст (поля по 4% с каждой стороны) — под неё ужимаем длинные строки.
        var textWidth = (int)(w * 0.92);

        var nowLabelY = (int)(h * 0.22);
        var nowTitleY = nowLabelY + gap;
        var nextLabelY = (int)(h * 0.60);
        var nextTitleY = nextLabelY + gap;

        var font = EscapePath(spec.FontFile);
        var outStart = Math.Max(0, target - 1);

        // Вход 0 — видеофон: загруженный файл (петля + масштаб/кроп), постер шоу (затемнённый) либо
        // анимированный градиент.
        var inputs = new List<string>();
        string videoPrefix;
        if (!string.IsNullOrEmpty(spec.BackgroundFile))
        {
            if (IsImage(spec.BackgroundFile))
                inputs.AddRange(["-loop", "1"]);
            else
                inputs.AddRange(["-stream_loop", "-1"]);
            inputs.AddRange(["-i", spec.BackgroundFile]);
            videoPrefix =
                $"[0:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps=30,format=yuv420p";
        }
        else if (!string.IsNullOrEmpty(spec.PosterFile))
        {
            // Постер (портрет) растягиваем на кадр, размываем и затемняем, чтобы текст читался.
            inputs.AddRange(["-loop", "1", "-i", spec.PosterFile]);
            videoPrefix =
                $"[0:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h}"
                + ",boxblur=6:1,eq=brightness=-0.28:saturation=0.9"
                + ",fps=30,format=yuv420p";
        }
        else
        {
            var gradient =
                $"gradients=s={w}x{h}:c0={spec.BackgroundColor}:c1={spec.BackgroundColor2}"
                + $":x0=0:y0=0:x1={w}:y1={h}:nb_colors=2:seed=42:speed=0.015:r=30:d={Fmt(target)}";
            inputs.AddRange(["-f", "lavfi", "-i", gradient]);
            videoPrefix = "[0:v]format=yuv420p";
        }

        // Вход 1 — звук: загруженная музыка (петля + loudnorm) либо синтезированный джингл.
        string audioChain;
        if (!string.IsNullOrEmpty(spec.MusicFile))
        {
            inputs.AddRange(["-stream_loop", "-1", "-i", spec.MusicFile]);
            audioChain =
                $"[1:a]loudnorm=I={_media.LoudnessTargetLufs.ToString(CultureInfo.InvariantCulture)}:TP=-1.5:LRA=11"
                + $",afade=t=in:d=0.5,afade=t=out:st={Fmt(outStart)}:d=1.0[a]";
        }
        else
        {
            var jingle =
                $"aevalsrc=exprs='0.14*sin(2*PI*523.25*t)+0.11*sin(2*PI*659.25*t)+0.09*sin(2*PI*783.99*t)'"
                + $":s=48000:d={Fmt(target)}";
            inputs.AddRange(["-f", "lavfi", "-i", jingle]);
            audioChain =
                $"[1:a]tremolo=f=4:d=0.4,afade=t=in:d=0.5,afade=t=out:st={Fmt(outStart)}:d=1.0,volume=0.7[a]";
        }

        var vchain = new StringBuilder(videoPrefix);
        if (spec.FreeText)
        {
            // Свободный текст: две центрированные строки (акцентная + основная), ужатые под ширину кадра.
            var line1Size = FitSize(spec.FreeLine1, labelSize + 4, textWidth);
            var line2Size = FitSize(spec.FreeLine2, titleSize, textWidth);
            var line1Y = (int)(h * 0.40);
            var line2Y = line1Y + (int)(line2Size * 1.2);
            vchain
                .Append(',')
                .Append(DrawTitle(font, text.Now, spec.AccentColor, line1Size, line1Y, 0.2));
            vchain
                .Append(',')
                .Append(DrawTitle(font, text.Next, spec.TextColor, line2Size, line2Y, 0.5));
        }
        else
        {
            var nowSize = FitSize(spec.NowTitle, titleSize, textWidth);
            var nextSize = FitSize(spec.NextTitle, titleSize, textWidth);
            vchain
                .Append(',')
                .Append(
                    DrawLabel(font, text.NowLabel, spec.AccentColor, labelSize, nowLabelY, 0.2)
                );
            vchain
                .Append(',')
                .Append(DrawTitle(font, text.Now, spec.TextColor, nowSize, nowTitleY, 0.3));
            vchain
                .Append(',')
                .Append(
                    DrawLabel(font, text.NextLabel, spec.AccentColor, labelSize, nextLabelY, 1.0)
                );
            vchain
                .Append(',')
                .Append(DrawTitle(font, text.Next, spec.TextColor, nextSize, nextTitleY, 1.1));
        }
        vchain.Append("[v]");

        var filterComplex = $"{vchain};{audioChain}";

        var args = new List<string> { "-hide_banner", "-nostdin", "-y" };
        args.AddRange(inputs);
        args.AddRange([
            "-filter_complex",
            filterComplex,
            "-map",
            "[v]",
            "-map",
            "[a]",
            "-threads",
            _media.TranscodeThreads.ToString(CultureInfo.InvariantCulture),
            "-c:v",
            "libx264",
            "-preset",
            "veryfast",
            "-crf",
            "21",
            "-pix_fmt",
            "yuv420p",
            "-force_key_frames",
            $"expr:gte(t,n_forced*{seg.ToString(CultureInfo.InvariantCulture)})",
            "-sc_threshold",
            "0",
            "-c:a",
            "aac",
            "-b:a",
            "128k",
            "-ac",
            "2",
            "-ar",
            "48000",
            "-t",
            Fmt(target),
            "-f",
            "hls",
            "-hls_time",
            seg.ToString(CultureInfo.InvariantCulture),
            "-hls_playlist_type",
            "vod",
            "-hls_list_size",
            "0",
            "-hls_segment_filename",
            Path.Combine(assetDir, "seg%05d.ts"),
            Path.Combine(assetDir, "index.m3u8"),
        ]);
        return args;
    }

    /// <summary>
    /// Подбирает размер шрифта так, чтобы строка влезла в <paramref name="maxWidth"/> по ширине:
    /// drawtext не умеет авто-fit, поэтому оцениваем ширину как len × 0.62 × fontsize (кириллица
    /// шире латиницы) и ужимаем базовый размер, но не мельче трети от него.
    /// </summary>
    private static int FitSize(string text, int baseSize, int maxWidth)
    {
        var len = string.IsNullOrEmpty(text) ? 1 : text.Length;
        var fit = (int)(maxWidth / (len * 0.62));
        return Math.Clamp(fit, Math.Max(12, baseSize / 3), baseSize);
    }

    private static readonly string[] ImageExtensions = [".jpg", ".jpeg", ".png", ".webp", ".bmp"];

    private static bool IsImage(string path) =>
        ImageExtensions.Contains(Path.GetExtension(path).ToLowerInvariant());

    private static string DrawTitle(
        string font,
        string textFile,
        string color,
        int size,
        int y,
        double fadeStart
    ) =>
        $"drawtext=fontfile={font}:textfile={EscapePath(textFile)}:expansion=none"
        + $":fontcolor={color}:fontsize={size}:x=(w-text_w)/2:y={y}"
        + ":shadowcolor=black@0.6:shadowx=2:shadowy=2"
        + $":alpha='{FadeExpr(fadeStart)}'";

    private static string DrawLabel(
        string font,
        string textFile,
        string color,
        int size,
        int y,
        double fadeStart
    ) =>
        // Подпись читается из файла (textfile=) с expansion=none — произвольные символы подписи
        // не могут сломать/инъектировать цепочку filter_complex (см. запись файлов в RenderAsync).
        $"drawtext=fontfile={font}:textfile={EscapePath(textFile)}:expansion=none"
        + $":fontcolor={color}:fontsize={size}:x=(w-text_w)/2:y={y}"
        + ":shadowcolor=black@0.6:shadowx=1:shadowy=1"
        + $":alpha='{FadeExpr(fadeStart)}'";

    private static string FadeExpr(double start) =>
        $"if(lt(t,{Fmt(start)}),0,min(1,(t-{Fmt(start)})/0.5))";

    /// <summary>Экранирование пути для значения опции фильтра (Windows-разделители → прямые слэши,
    /// двоеточие экранируется). На Linux (контейнере) — фактически no-op.</summary>
    private static string EscapePath(string path) => path.Replace('\\', '/').Replace(":", "\\:");

    private static void TryDelete(string path)
    {
        try
        {
            if (File.Exists(path))
                File.Delete(path);
        }
        catch (IOException)
        {
            // Файл-подсказка для drawtext; не критично, если не удалился.
        }
    }
}
