using Models.Settings;
using Api.Services.Contracts;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Swashbuckle.AspNetCore.Annotations;
using Common.Responses;
using Microsoft.AspNetCore.RateLimiting;

namespace Api.Controllers
{
    /// <summary>
    /// Controller for handling file downloads with support for resume and chunked transfers.
    /// Routes are prefixed with "api/filedownload".
    /// </summary>
    [ApiController]
    [Route("api/[controller]")]
    [EnableCors("FrontendOnly")]
    [EnableRateLimiting("download")]
    public sealed class FileDownloadController : ControllerBase
    {
        private readonly ILogger<FileDownloadController> _logger;
        private readonly FileStorageSettings _fileStorageSettings;
        private readonly IContentTypeProvider _contentTypeProvider;
        private readonly IEmailSender _emailSender;
        private readonly ICaptchaVerifier _captcha;
        private const int BufferSize = 81920;

        public FileDownloadController(
            ILogger<FileDownloadController> logger,
            IOptions<FileStorageSettings> fileStorageSettings,
            IContentTypeProvider contentTypeProvider,
            IEmailSender emailSender,
            ICaptchaVerifier captcha)
        {
            _logger = logger;
            _fileStorageSettings = fileStorageSettings.Value;
            _contentTypeProvider = contentTypeProvider;
            _emailSender = emailSender;
            _captcha = captcha;
        }

        /// <summary>
        /// Downloads a file with support for resume (range requests) and chunked transfer.
        /// Supports HTTP 206 Partial Content for efficient downloads and resume capability.
        /// Requires a valid reCAPTCHA v3 token on the initial (non-range) request.
        /// Sends email notification when download starts.
        /// </summary>
        /// <param name="fileName">The name of the file to download (optional - uses default from settings if not provided).</param>
        /// <param name="captchaToken">reCAPTCHA v3 token — required on the initial download request; omit on subsequent range requests.</param>
        /// <returns>File stream with appropriate headers for resumable downloads</returns>
        [HttpGet("{fileName?}")]
        [SwaggerOperation(Summary = "Download file", Description = "Downloads a file. Requires a reCAPTCHA v3 token on the initial request. Range requests for resume do not require a token.")]
        [SwaggerResponse(StatusCodes.Status200OK, "Full file content returned")]
        [SwaggerResponse(StatusCodes.Status206PartialContent, "Partial file content returned for a range request")]
        [SwaggerResponse(StatusCodes.Status400BadRequest, "Missing/invalid captcha token, no file name, or no default configured")]
        [SwaggerResponse(StatusCodes.Status404NotFound, "Requested file was not found")]
        [SwaggerResponse(StatusCodes.Status416RangeNotSatisfiable, "Requested byte range is invalid")]
        [SwaggerResponse(StatusCodes.Status500InternalServerError, "Unexpected server error while downloading")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status206PartialContent)]
        [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
        [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)]
        [ProducesResponseType(StatusCodes.Status416RangeNotSatisfiable)]
        [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
        public async Task<IActionResult> DownloadFile(string? fileName = null, [FromQuery] string? captchaToken = null)
        {
            try
            {
                var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();

                // Captcha required on the initial (full) download only — range requests are resume continuations.
                var isRangeRequest = !string.IsNullOrEmpty(Request.Headers[HeaderNames.Range].ToString());
                if (!isRangeRequest)
                {
                    if (string.IsNullOrWhiteSpace(captchaToken))
                    {
                        _logger.LogWarning("Download attempt without captcha token from IP={IP}", HttpContext.Connection.RemoteIpAddress);
                        return BadRequest(new ErrorResponse { Error = "Captcha token is required.", Code = "captcha_token_missing" });
                    }

                    var verdict = await _captcha.VerifyAsync(captchaToken, userIp, "file_download", CancellationToken.None);
                    if (!verdict.Success)
                    {
                        _logger.LogWarning("Download blocked by captcha. IP={IP} Score={Score}", userIp, verdict.Score);
                        return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "captcha_verification_failed" });
                    }
                }

                if (string.IsNullOrWhiteSpace(fileName))
                {
                    fileName = _fileStorageSettings.DefaultFileName;

                    if (string.IsNullOrWhiteSpace(fileName))
                    {
                        _logger.LogWarning("No file name provided and no default file name configured");
                        return BadRequest(new ErrorResponse { Error = "File name is required", Code = "file_name_required" });
                    }

                    _logger.LogInformation("Using default file name from settings: {FileName}", fileName);
                }

                var fileStoragePath = _fileStorageSettings.Path;

                if (!Path.IsPathRooted(fileStoragePath))
                {
                    var solutionRoot = Directory.GetCurrentDirectory();
                    if (solutionRoot.EndsWith("api", StringComparison.OrdinalIgnoreCase))
                        solutionRoot = Directory.GetParent(solutionRoot)?.FullName ?? solutionRoot;
                    fileStoragePath = Path.Combine(solutionRoot, fileStoragePath);
                }

                var sanitizedFileName = Path.GetFileName(fileName);
                var filePath = Path.Combine(fileStoragePath, sanitizedFileName);

                if (!System.IO.File.Exists(filePath))
                {
                    _logger.LogWarning("File not found: {FilePath}", filePath);
                    return NotFound(new ErrorResponse { Error = "File not found", Code = "file_not_found" });
                }

                var fileLength = new FileInfo(filePath).Length;

                if (!_contentTypeProvider.TryGetContentType(filePath, out var contentType))
                    contentType = "application/octet-stream";

                _ = Task.Run(async () =>
                {
                    try
                    {
                        await _emailSender.SendFileDownloadNotificationAsync(sanitizedFileName, userIp, CancellationToken.None);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Failed to send file download notification for {FileName}", sanitizedFileName);
                    }
                });

                var rangeHeader = Request.Headers[HeaderNames.Range].ToString();
                if (!string.IsNullOrEmpty(rangeHeader))
                    return await HandleRangeRequest(filePath, fileLength, contentType, rangeHeader, sanitizedFileName);

                _logger.LogInformation("Starting full file download: {FileName} ({FileSize} bytes)", sanitizedFileName, fileLength);

                var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, useAsync: true);
                Response.Headers.Append(HeaderNames.AcceptRanges, "bytes");
                Response.Headers.Append(HeaderNames.ContentLength, fileLength.ToString());

                return File(stream, contentType, sanitizedFileName, enableRangeProcessing: true);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error downloading file: {FileName}", fileName);
                return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse { Error = "An error occurred while downloading the file", Code = "download_failed" });
            }
        }

        /// <summary>
        /// Handles HTTP range requests for partial content downloads and resume support.
        /// </summary>
        private async Task<IActionResult> HandleRangeRequest(
            string filePath,
            long fileLength,
            string contentType,
            string rangeHeader,
            string fileName)
        {
            try
            {
                var range = rangeHeader.Replace("bytes=", "").Split('-');

                long startByte = 0;
                long endByte = fileLength - 1;

                if (!string.IsNullOrEmpty(range[0]))
                    startByte = long.Parse(range[0]);

                if (range.Length > 1 && !string.IsNullOrEmpty(range[1]))
                    endByte = long.Parse(range[1]);

                if (startByte > endByte || startByte >= fileLength)
                {
                    _logger.LogWarning("Invalid range request: {Range} for file size {FileLength}", rangeHeader, fileLength);
                    return StatusCode(StatusCodes.Status416RangeNotSatisfiable);
                }

                if (endByte >= fileLength)
                    endByte = fileLength - 1;

                var contentLength = endByte - startByte + 1;

                _logger.LogInformation(
                    "Range request for {FileName}: bytes {Start}-{End}/{Total} ({ContentLength} bytes)",
                    fileName, startByte, endByte, fileLength, contentLength);

                var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, useAsync: true);
                stream.Seek(startByte, SeekOrigin.Begin);

                Response.StatusCode = StatusCodes.Status206PartialContent;
                Response.Headers.Append(HeaderNames.AcceptRanges, "bytes");
                Response.Headers.Append(HeaderNames.ContentRange, $"bytes {startByte}-{endByte}/{fileLength}");
                Response.Headers.Append(HeaderNames.ContentLength, contentLength.ToString());
                Response.ContentType = contentType;

                await StreamRangeAsync(stream, Response.Body, contentLength);
                await stream.DisposeAsync();

                return new EmptyResult();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing range request for file: {FileName}", fileName);
                return StatusCode(StatusCodes.Status500InternalServerError);
            }
        }

        /// <summary>
        /// Efficiently streams a specific byte range from source to destination.
        /// </summary>
        private static async Task StreamRangeAsync(Stream source, Stream destination, long bytesToRead)
        {
            var buffer = new byte[BufferSize];
            long totalBytesRead = 0;

            while (totalBytesRead < bytesToRead)
            {
                var bytesToReadThisIteration = (int)Math.Min(BufferSize, bytesToRead - totalBytesRead);
                var bytesRead = await source.ReadAsync(buffer.AsMemory(0, bytesToReadThisIteration));

                if (bytesRead == 0)
                    break;

                await destination.WriteAsync(buffer.AsMemory(0, bytesRead));
                totalBytesRead += bytesRead;
            }

            await destination.FlushAsync();
        }
    }
}
