using System.Diagnostics;
using FrameProcessor.Concurrency;
using FrameProcessor.Configuration;
using FrameProcessor.Domain;
using FrameProcessor.ImagePipeline;
using FrameProcessor.Mqtt;
using FrameProcessor.Storage;
using FrameProcessor.UrlFetch;
using Microsoft.AspNetCore.Mvc;

namespace FrameProcessor.Controllers;

[ApiController]
[Route("api/frames")]
public sealed class FramesController : ControllerBase
{
    private readonly FramesRegistry _frames;
    private readonly IImagePipeline _pipeline;
    private readonly ImageStore _store;
    private readonly MqttPublisher _mqtt;
    private readonly IImageUrlFetcher _urlFetcher;
    private readonly FrameLockProvider _locks;
    private readonly ILogger<FramesController> _logger;

    public FramesController(
        FramesRegistry frames,
        IImagePipeline pipeline,
        ImageStore store,
        MqttPublisher mqtt,
        IImageUrlFetcher urlFetcher,
        FrameLockProvider locks,
        ILogger<FramesController> logger)
    {
        _frames = frames;
        _pipeline = pipeline;
        _store = store;
        _mqtt = mqtt;
        _urlFetcher = urlFetcher;
        _locks = locks;
        _logger = logger;
    }

    [HttpPost("{name}/image")]
    [Consumes("multipart/form-data")]
    public async Task<IActionResult> UploadImage(
        string name,
        IFormFile image,
        CancellationToken cancellationToken)
    {
        var stopwatch = Stopwatch.StartNew();

        if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame))
        {
            return NotFound();
        }

        var file = image;
        if (file is null || file.Length == 0)
        {
            return BadRequest(new { error = "Missing 'image' file part." });
        }

        using (await _locks.AcquireAsync(frame.Name, cancellationToken).ConfigureAwait(false))
        {
            byte[] pngBytes;
            await using (var stream = file.OpenReadStream())
            {
                pngBytes = _pipeline.Process(stream, frame);
            }

            return await FinishUploadAsync(frame, pngBytes, "file", file.Length, stopwatch, cancellationToken)
                .ConfigureAwait(false);
        }
    }

    [HttpPost("{name}/image-url")]
    [Consumes("application/json")]
    public async Task<IActionResult> UploadImageUrl(
        string name,
        [FromBody] ImageUrlRequest? body,
        CancellationToken cancellationToken)
    {
        var stopwatch = Stopwatch.StartNew();

        if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame))
        {
            return NotFound();
        }

        if (body is null || string.IsNullOrWhiteSpace(body.Url) ||
            !Uri.TryCreate(body.Url, UriKind.Absolute, out var uri) ||
            (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
        {
            return BadRequest(new { error = "Missing or invalid 'url'." });
        }

        using (await _locks.AcquireAsync(frame.Name, cancellationToken).ConfigureAwait(false))
        {
            Stream source;
            try
            {
                source = await _urlFetcher.FetchAsync(uri, cancellationToken).ConfigureAwait(false);
            }
            catch (ImageFetchException ex)
            {
                return StatusCode(StatusCodes.Status502BadGateway, new { error = ex.Message });
            }

            byte[] pngBytes;
            long inputBytes;
            await using (source)
            {
                inputBytes = source.CanSeek ? source.Length : 0;
                pngBytes = _pipeline.Process(source, frame);
            }

            return await FinishUploadAsync(frame, pngBytes, "url", inputBytes, stopwatch, cancellationToken)
                .ConfigureAwait(false);
        }
    }

    private async Task<IActionResult> FinishUploadAsync(
        Frame frame,
        byte[] pngBytes,
        string inputSource,
        long inputBytes,
        Stopwatch stopwatch,
        CancellationToken cancellationToken)
    {
        await _store.WriteAsync(frame.Mac, pngBytes, cancellationToken).ConfigureAwait(false);

        var publishResult = await _mqtt.PublishAsync(frame.Mac, cancellationToken).ConfigureAwait(false);
        var mqttPublished = publishResult == PublishResult.Success;

        stopwatch.Stop();

        _logger.LogInformation(
            "Processed upload for {FrameName} ({MacAddress}) source={InputSource} in={InputBytes} out={OutputBytes} elapsed={ElapsedMs}ms mqtt={MqttPublished}",
            frame.Name.Value,
            frame.Mac.ToString(),
            inputSource,
            inputBytes,
            pngBytes.LongLength,
            stopwatch.ElapsedMilliseconds,
            mqttPublished);

        return Ok(new
        {
            frame = frame.Name.Value,
            mac = frame.Mac.ToString(),
            url = $"/i/{frame.Mac}.png",
            processedAt = DateTimeOffset.UtcNow,
            mqttPublished,
        });
    }

}

public sealed record ImageUrlRequest(string? Url);
