using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SRFPlay.Api;
using Jellyfin.Plugin.SRFPlay.Api.Models;
using Jellyfin.Plugin.SRFPlay.Api.Models.PlayV3;
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
using Jellyfin.Plugin.SRFPlay.Utilities;
using MediaBrowser.Controller;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Logging;

namespace Jellyfin.Plugin.SRFPlay.Services;

/// <summary>
/// Service for managing sport livestream recordings using ffmpeg.
/// </summary>
public class RecordingService : IRecordingService, IDisposable
{
    private readonly ILogger<RecordingService> _logger;
    private readonly ISRFApiClientFactory _apiClientFactory;
    private readonly IStreamProxyService _proxyService;
    private readonly IStreamUrlResolver _streamUrlResolver;
    private readonly IMediaCompositionFetcher _mediaCompositionFetcher;
    private readonly IServerApplicationHost _appHost;
    private readonly IMediaEncoder _mediaEncoder;
    private readonly ConcurrentDictionary<string, Process> _activeProcesses = new();
    private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
    private readonly SemaphoreSlim _persistLock = new(1, 1);
    private readonly SemaphoreSlim _processLock = new(1, 1);
    private List<RecordingEntry> _recordings = new();
    private bool _loaded;
    private bool _disposed;

    /// <summary>
    /// Initializes a new instance of the <see cref="RecordingService"/> class.
    /// </summary>
    /// <param name="logger">The logger.</param>
    /// <param name="apiClientFactory">The API client factory.</param>
    /// <param name="proxyService">The stream proxy service.</param>
    /// <param name="streamUrlResolver">The stream URL resolver.</param>
    /// <param name="mediaCompositionFetcher">The media composition fetcher.</param>
    /// <param name="appHost">The application host.</param>
    /// <param name="mediaEncoder">The media encoder for ffmpeg path.</param>
    public RecordingService(
        ILogger<RecordingService> logger,
        ISRFApiClientFactory apiClientFactory,
        IStreamProxyService proxyService,
        IStreamUrlResolver streamUrlResolver,
        IMediaCompositionFetcher mediaCompositionFetcher,
        IServerApplicationHost appHost,
        IMediaEncoder mediaEncoder)
    {
        _logger = logger;
        _apiClientFactory = apiClientFactory;
        _proxyService = proxyService;
        _streamUrlResolver = streamUrlResolver;
        _mediaCompositionFetcher = mediaCompositionFetcher;
        _appHost = appHost;
        _mediaEncoder = mediaEncoder;
    }

    private string GetDataFilePath()
    {
        var dataPath = Plugin.Instance?.DataFolderPath ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "jellyfin", "plugins", "SRFPlay");
        Directory.CreateDirectory(dataPath);
        return Path.Combine(dataPath, "recordings.json");
    }

    private string GetRecordingOutputPath()
    {
        var config = Plugin.Instance?.Configuration;
        var path = config?.RecordingOutputPath;
        if (string.IsNullOrWhiteSpace(path))
        {
            path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "SRFRecordings");
        }

        Directory.CreateDirectory(path);
        return path;
    }

    private string GetServerBaseUrl()
    {
        var config = Plugin.Instance?.Configuration;
        if (config != null && !string.IsNullOrWhiteSpace(config.PublicServerUrl))
        {
            return config.PublicServerUrl.TrimEnd('/');
        }

        // For local ffmpeg access, use localhost directly
        return "http://localhost:8096";
    }

    private async Task LoadRecordingsAsync()
    {
        if (_loaded)
        {
            return;
        }

        var filePath = GetDataFilePath();
        if (File.Exists(filePath))
        {
            try
            {
                var json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
                _recordings = JsonSerializer.Deserialize<List<RecordingEntry>>(json) ?? new List<RecordingEntry>();
                _logger.LogInformation("Loaded {Count} recording entries from {Path}", _recordings.Count, filePath);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to load recordings from {Path}", filePath);
                _recordings = new List<RecordingEntry>();
            }
        }

        _loaded = true;
    }

    private async Task SaveRecordingsAsync()
    {
        await _persistLock.WaitAsync().ConfigureAwait(false);
        try
        {
            var filePath = GetDataFilePath();
            var json = JsonSerializer.Serialize(_recordings, _jsonOptions);
            await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to save recordings");
        }
        finally
        {
            _persistLock.Release();
        }
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<PlayV3TvProgram>> GetUpcomingScheduleAsync(CancellationToken cancellationToken)
    {
        var units = System.Enum.GetValues<Configuration.BusinessUnit>();

        using var apiClient = _apiClientFactory.CreateClient();

        // Aggregate sport livestreams across every business unit so the recordings
        // page shows events from all languages.
        var all = new List<PlayV3TvProgram>();
        foreach (var unit in units)
        {
            var businessUnit = unit.ToString().ToLowerInvariant();
            var livestreams = await apiClient.GetScheduledLivestreamsAsync(businessUnit, "SPORT", cancellationToken).ConfigureAwait(false);
            if (livestreams != null)
            {
                all.AddRange(livestreams);
            }
        }

        // Filter to only future/current livestreams that aren't blocked
        return all
            .Where(ls => ls.Blocked != true && (ls.ValidTo == null || ls.ValidTo.Value.ToUniversalTime() > DateTime.UtcNow))
            .OrderBy(ls => ls.ValidFrom)
            .ToList();
    }

    /// <inheritdoc />
    public async Task<RecordingEntry> ScheduleRecordingAsync(string urn, CancellationToken cancellationToken)
    {
        await LoadRecordingsAsync().ConfigureAwait(false);

        // Check if already scheduled
        var existing = _recordings.FirstOrDefault(r => r.Urn == urn && r.State is RecordingState.Scheduled or RecordingState.WaitingForStream or RecordingState.Recording);
        if (existing != null)
        {
            _logger.LogInformation("Recording already exists for URN {Urn} in state {State}", urn, existing.State);
            return existing;
        }

        // Fetch metadata for the URN. The unit is encoded in the URN itself, so use that to
        // query the right schedule regardless of which units are enabled.
        var config = Plugin.Instance?.Configuration;
        var businessUnit = ParseBusinessUnitFromUrn(urn)
            ?? (config?.BusinessUnit ?? Configuration.BusinessUnit.SRF).ToString().ToLowerInvariant();

        using var apiClient = _apiClientFactory.CreateClient();
        var livestreams = await apiClient.GetScheduledLivestreamsAsync(businessUnit, "SPORT", cancellationToken).ConfigureAwait(false);
        var program = livestreams?.FirstOrDefault(ls => ls.Urn == urn);

        var entry = new RecordingEntry
        {
            Id = Guid.NewGuid().ToString("N"),
            Urn = urn,
            BusinessUnit = ParseBusinessUnitFromUrn(urn) ?? businessUnit,
            Title = program?.Title ?? urn,
            Description = program?.Lead ?? program?.Description,
            ImageUrl = program?.ImageUrl,
            ValidFrom = program?.ValidFrom,
            ValidTo = program?.ValidTo,
            State = RecordingState.Scheduled,
            CreatedAt = DateTime.UtcNow
        };

        _recordings.Add(entry);
        await SaveRecordingsAsync().ConfigureAwait(false);

        _logger.LogInformation("Scheduled recording for '{Title}' (URN: {Urn}, starts: {ValidFrom})", entry.Title, urn, entry.ValidFrom);
        return entry;
    }

    /// <summary>
    /// Extracts the business unit from an SRF URN of the form "urn:&lt;bu&gt;:&lt;type&gt;:...".
    /// </summary>
    /// <param name="urn">The URN.</param>
    /// <returns>The lowercase business unit, or null if it cannot be determined.</returns>
    private static string? ParseBusinessUnitFromUrn(string urn)
    {
        if (string.IsNullOrEmpty(urn))
        {
            return null;
        }

        var parts = urn.Split(':');
        return parts.Length >= 2 && !string.IsNullOrEmpty(parts[1])
            ? parts[1].ToLowerInvariant()
            : null;
    }

    /// <inheritdoc />
    public bool CancelRecording(string recordingId)
    {
        var entry = _recordings.FirstOrDefault(r => r.Id == recordingId);
        if (entry == null)
        {
            return false;
        }

        if (entry.State == RecordingState.Recording)
        {
            StopFfmpeg(recordingId);
        }

        entry.State = RecordingState.Cancelled;
        entry.RecordingEndedAt = DateTime.UtcNow;
        _ = SaveRecordingsAsync();

        _logger.LogInformation("Cancelled recording '{Title}' ({Id})", entry.Title, recordingId);
        return true;
    }

    /// <inheritdoc />
    public bool StopRecording(string recordingId)
    {
        var entry = _recordings.FirstOrDefault(r => r.Id == recordingId && r.State == RecordingState.Recording);
        if (entry == null)
        {
            return false;
        }

        StopFfmpeg(recordingId);

        entry.State = RecordingState.Completed;
        entry.RecordingEndedAt = DateTime.UtcNow;

        if (entry.OutputPath != null && File.Exists(entry.OutputPath))
        {
            entry.FileSizeBytes = new FileInfo(entry.OutputPath).Length;
        }

        _ = SaveRecordingsAsync();

        _logger.LogInformation("Stopped recording '{Title}' ({Id})", entry.Title, recordingId);
        return true;
    }

    /// <inheritdoc />
    public IReadOnlyList<RecordingEntry> GetRecordings(RecordingState? stateFilter)
    {
        // Ensure loaded synchronously for simple reads
        if (!_loaded)
        {
            LoadRecordingsAsync().GetAwaiter().GetResult();
        }

        if (stateFilter.HasValue)
        {
            return _recordings.Where(r => r.State == stateFilter.Value).OrderByDescending(r => r.CreatedAt).ToList();
        }

        return _recordings.OrderByDescending(r => r.CreatedAt).ToList();
    }

    /// <inheritdoc />
    public bool DeleteRecording(string recordingId, bool deleteFile)
    {
        var entry = _recordings.FirstOrDefault(r => r.Id == recordingId);
        if (entry == null)
        {
            return false;
        }

        if (entry.State == RecordingState.Recording)
        {
            StopFfmpeg(recordingId);
        }

        if (deleteFile && !string.IsNullOrEmpty(entry.OutputPath) && File.Exists(entry.OutputPath))
        {
            try
            {
                File.Delete(entry.OutputPath);
                _logger.LogInformation("Deleted recording file: {Path}", entry.OutputPath);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to delete recording file: {Path}", entry.OutputPath);
            }
        }

        _recordings.Remove(entry);
        _ = SaveRecordingsAsync();

        _logger.LogInformation("Deleted recording entry '{Title}' ({Id})", entry.Title, recordingId);
        return true;
    }

    /// <inheritdoc />
    public async Task ProcessRecordingsAsync(CancellationToken cancellationToken)
    {
        // Prevent overlapping scheduler runs from spawning duplicate ffmpeg processes
        if (!await _processLock.WaitAsync(0, CancellationToken.None).ConfigureAwait(false))
        {
            _logger.LogDebug("ProcessRecordingsAsync already running, skipping");
            return;
        }

        try
        {
            await ProcessRecordingsCoreAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _processLock.Release();
        }
    }

    private async Task ProcessRecordingsCoreAsync(CancellationToken cancellationToken)
    {
        await LoadRecordingsAsync().ConfigureAwait(false);

        var now = DateTime.UtcNow;
        var changed = false;

        foreach (var entry in _recordings.ToList())
        {
            // Normalize ValidFrom/ValidTo to UTC for correct comparison
            var validFromUtc = entry.ValidFrom.HasValue ? entry.ValidFrom.Value.ToUniversalTime() : (DateTime?)null;
            var validToUtc = entry.ValidTo.HasValue ? entry.ValidTo.Value.ToUniversalTime() : (DateTime?)null;

            switch (entry.State)
            {
                case RecordingState.Scheduled:
                case RecordingState.WaitingForStream:
                    // Check if it's time to start recording
                    if (validFromUtc.HasValue && validFromUtc.Value <= now.AddMinutes(2))
                    {
                        _logger.LogInformation(
                            "Time to start recording '{Title}': ValidFrom={ValidFrom} (UTC: {ValidFromUtc}), Now={Now}",
                            entry.Title,
                            entry.ValidFrom,
                            validFromUtc,
                            now);
                        changed |= await TryStartRecordingAsync(entry, cancellationToken).ConfigureAwait(false);
                    }

                    break;

                case RecordingState.Recording:
                    // Check if recording should stop (ValidTo reached or process died)
                    if (validToUtc.HasValue && validToUtc.Value <= now)
                    {
                        _logger.LogInformation("Recording '{Title}' reached ValidTo, stopping", entry.Title);
                        StopFfmpeg(entry.Id);
                        entry.State = RecordingState.Completed;
                        entry.RecordingEndedAt = now;
                        if (entry.OutputPath != null && File.Exists(entry.OutputPath))
                        {
                            entry.FileSizeBytes = new FileInfo(entry.OutputPath).Length;
                        }

                        changed = true;
                    }
                    else if (!_activeProcesses.ContainsKey(entry.Id))
                    {
                        // ffmpeg process died unexpectedly — try to restart
                        _logger.LogWarning("ffmpeg process for '{Title}' is no longer running, attempting restart", entry.Title);
                        changed |= await TryStartRecordingAsync(entry, cancellationToken).ConfigureAwait(false);
                    }

                    break;
            }
        }

        if (changed)
        {
            await SaveRecordingsAsync().ConfigureAwait(false);
        }
    }

    private async Task<bool> TryStartRecordingAsync(RecordingEntry entry, CancellationToken cancellationToken)
    {
        try
        {
            // Fetch the media composition to get the stream URL
            var mediaComposition = await _mediaCompositionFetcher.GetMediaCompositionAsync(entry.Urn, cacheDurationOverride: 2, cancellationToken: cancellationToken).ConfigureAwait(false);
            var chapter = mediaComposition?.ChapterList is { Count: > 0 } list ? list[0] : null;

            if (chapter == null)
            {
                _logger.LogDebug("No chapter found for '{Title}', stream may not be live yet", entry.Title);
                entry.State = RecordingState.WaitingForStream;
                return true;
            }

            var config = Plugin.Instance?.Configuration;
            var quality = config?.QualityPreference ?? Configuration.QualityPreference.Auto;
            var streamUrl = _streamUrlResolver.GetStreamUrl(chapter, quality);

            if (string.IsNullOrEmpty(streamUrl))
            {
                _logger.LogDebug("No stream URL available for '{Title}', waiting", entry.Title);
                entry.State = RecordingState.WaitingForStream;
                return true;
            }

            // Register the stream with the proxy so we can use the proxy URL
            var itemId = $"rec_{entry.Id}";
            var isLiveStream = chapter.Type == "SCHEDULED_LIVESTREAM" || UrnHelper.IsLivestreamUrn(entry.Urn);
            _proxyService.RegisterStreamDeferred(itemId, streamUrl, entry.Urn, isLiveStream);

            // Build proxy URL for ffmpeg (use localhost for local access)
            var proxyUrl = $"{GetServerBaseUrl()}/Plugins/SRFPlay/Proxy/{itemId}/master.m3u8";

            // Build output file path
            var safeTitle = SanitizeFileName(entry.Title);
            var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HHmm", CultureInfo.InvariantCulture);
            var outputPath = Path.Combine(GetRecordingOutputPath(), $"{safeTitle}_{timestamp}.mkv");
            entry.OutputPath = outputPath;

            // Start ffmpeg
            StartFfmpeg(entry.Id, proxyUrl, outputPath);

            entry.State = RecordingState.Recording;
            entry.RecordingStartedAt = DateTime.UtcNow;

            _logger.LogInformation("Started recording '{Title}' to {OutputPath}", entry.Title, outputPath);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to start recording '{Title}'", entry.Title);
            entry.State = RecordingState.Failed;
            entry.ErrorMessage = ex.Message;
            return true;
        }
    }

    private void StartFfmpeg(string recordingId, string inputUrl, string outputPath)
    {
        var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = _mediaEncoder.EncoderPath,
                Arguments = $"-y -i \"{inputUrl}\" -c copy -movflags +faststart \"{outputPath}\"",
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardError = true,
                CreateNoWindow = true
            },
            EnableRaisingEvents = true
        };

        process.ErrorDataReceived += (_, args) =>
        {
            if (!string.IsNullOrEmpty(args.Data))
            {
                _logger.LogDebug("ffmpeg [{RecordingId}]: {Data}", recordingId, args.Data);
            }
        };

        process.Exited += (_, _) =>
        {
            _logger.LogInformation("ffmpeg process exited for recording {RecordingId} with code {ExitCode}", recordingId, process.ExitCode);
            _activeProcesses.TryRemove(recordingId, out _);
        };

        process.Start();
        process.BeginErrorReadLine();

        _activeProcesses[recordingId] = process;
        _logger.LogInformation("Started ffmpeg (PID {Pid}) for recording {RecordingId}: {Args}", process.Id, recordingId, process.StartInfo.Arguments);
    }

    private void StopFfmpeg(string recordingId)
    {
        if (_activeProcesses.TryRemove(recordingId, out var process))
        {
            try
            {
                if (!process.HasExited)
                {
                    // Send 'q' to ffmpeg stdin for graceful shutdown
                    process.StandardInput.Write("q");
                    process.StandardInput.Flush();

                    if (!process.WaitForExit(10000))
                    {
                        _logger.LogWarning("ffmpeg did not exit gracefully for {RecordingId}, killing", recordingId);
                        process.Kill(true);
                    }
                }

                process.Dispose();
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Error stopping ffmpeg for recording {RecordingId}", recordingId);
            }
        }
    }

    private static string SanitizeFileName(string name)
    {
        var invalid = Path.GetInvalidFileNameChars();
        var sanitized = string.Join("_", name.Split(invalid, StringSplitOptions.RemoveEmptyEntries));
        // Also replace spaces and other problematic chars
        sanitized = Regex.Replace(sanitized, @"[\s]+", "_");
        return sanitized.Length > 100 ? sanitized[..100] : sanitized;
    }

    /// <inheritdoc />
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Releases resources.
    /// </summary>
    /// <param name="disposing">True to release managed resources.</param>
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }

        if (disposing)
        {
            foreach (var kvp in _activeProcesses)
            {
                StopFfmpeg(kvp.Key);
            }

            _persistLock.Dispose();
            _processLock.Dispose();
        }

        _disposed = true;
    }
}
