using System.Globalization;
using System.Text;
using Microsoft.Extensions.Options;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Domain.Broadcast;
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>
    /// Одна строка, разложенная под drawtext: файл с текстом, размер, цвет и вертикальная позиция.
    /// Текст ffmpeg читает через <c>textfile=</c> с <c>expansion=none</c>: иначе запятая, <c>;</c>,
    /// <c>[</c> или <c>]</c> в тексте ломают (или инъектируют звенья в) цепочку <c>-filter_complex</c>.
    /// </summary>
    private sealed record LayoutLine(
        string File,
        string Text,
        int FontSize,
        string Color,
        int Y,
        bool Shadow,
        double FadeStart
    );

    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 layout = Layout(assetDir, spec);
        foreach (var line in layout)
            await File.WriteAllTextAsync(
                line.File,
                line.Text,
                new UTF8Encoding(false),
                cancellationToken
            );

        try
        {
            var args = BuildArgs(assetDir, seg, target, layout, 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
        {
            foreach (var line in layout)
                TryDelete(line.File);
        }
    }

    /// <summary>
    /// Раскладывает строки по кадру: блок центрируется целиком по вертикали, размер зависит от роли
    /// и ужимается под ширину, строки проявляются по очереди. Фиксированных мест у ролей нет —
    /// иначе набор из двух строк висел бы в верхней трети кадра, как это было у «Сейчас/Далее».
    /// </summary>
    private static List<LayoutLine> Layout(string assetDir, BumperRenderSpec spec)
    {
        var h = spec.Height;
        var titleSize = Math.Max(24, h / 10);
        var labelSize = Math.Max(14, h / 22);
        var captionSize = Math.Max(12, h / 28);
        var textWidth = (int)(spec.Width * 0.92); // Поля по 4% с каждой стороны.

        var sizes = spec
            .Lines.Select(line =>
            {
                var baseSize = line.Style switch
                {
                    BumperLineStyle.Title => titleSize,
                    BumperLineStyle.Caption => captionSize,
                    _ => labelSize,
                };
                return FitSize(line.Text, baseSize, textWidth);
            })
            .ToList();

        // Между подписью и её названием зазор меньше, чем между смысловыми блоками.
        var gaps = new List<int>();
        for (var i = 1; i < spec.Lines.Count; i++)
            gaps.Add(
                spec.Lines[i - 1].Style == BumperLineStyle.Label
                    ? (int)(sizes[i] * 0.25)
                    : (int)(sizes[i] * 0.8)
            );

        var totalHeight = sizes.Sum(s => (int)(s * 1.2)) + gaps.Sum();
        var y = (h - totalHeight) / 2;

        var layout = new List<LayoutLine>(spec.Lines.Count);
        for (var i = 0; i < spec.Lines.Count; i++)
        {
            if (i > 0)
                y += (int)(sizes[i - 1] * 1.2) + gaps[i - 1];

            var line = spec.Lines[i];
            layout.Add(
                new LayoutLine(
                    Path.Combine(assetDir, $"line{i}.txt"),
                    line.Text,
                    sizes[i],
                    line.Color == BumperLineColor.Accent ? spec.AccentColor : spec.TextColor,
                    y,
                    line.Style == BumperLineStyle.Title,
                    0.2 + i * 0.3
                )
            );
        }

        return layout;
    }

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

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

        // Вход 0 — видеофон: загруженный файл (петля + масштаб/кроп), постер шоу (затемнённый) либо
        // анимированный градиент.
        var inputs = new List<string>();
        string videoPrefix;
        if (!string.IsNullOrEmpty(spec.ClipFile))
        {
            // Готовый ролик как подложка. Не зацикливаем: длину задаёт он сам, а выравнивание по
            // сегменту добираем стоп-кадром — повтор первых секунд в конце заметнее любой заморозки.
            inputs.AddRange(["-i", spec.ClipFile]);
            videoPrefix =
                $"[0:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h}"
                + $",tpad=stop_mode=clone:stop_duration={Fmt(target)},fps=30,format=yuv420p";
        }
        else 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 (spec.UseClipAudio)
        {
            audioChain =
                $"[0:a]loudnorm=I={_media.LoudnessTargetLufs.ToString(CultureInfo.InvariantCulture)}:TP=-1.5:LRA=11"
                // apad добивает тишиной хвост, дорисованный стоп-кадром: без него поток звука
                // кончается раньше видео и ffmpeg обрезает кадр по нему.
                + $",apad,afade=t=out:st={Fmt(outStart)}:d=1.0[a]";
        }
        else 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);
        foreach (var line in layout)
            vchain.Append(',').Append(DrawLine(font, line));
        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());

    /// <summary>
    /// Звено drawtext для одной строки. Текст читается из файла (textfile=) с expansion=none —
    /// произвольные символы не могут сломать/инъектировать цепочку filter_complex.
    /// </summary>
    private static string DrawLine(string font, LayoutLine line)
    {
        var shadow = line.Shadow
            ? ":shadowcolor=black@0.6:shadowx=2:shadowy=2"
            : ":shadowcolor=black@0.6:shadowx=1:shadowy=1";
        return $"drawtext=fontfile={font}:textfile={EscapePath(line.File)}:expansion=none"
            + $":fontcolor={line.Color}:fontsize={line.FontSize}:x=(w-text_w)/2:y={line.Y}"
            + shadow
            + $":alpha='{FadeExpr(line.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; не критично, если не удалился.
        }
    }
}
