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 Shared.Models.Responses;

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")]
    public sealed class FileDownloadController : ControllerBase
    {
        private readonly ILogger<FileDownloadController> _logger;
        private readonly FileStorageSettings _fileStorageSettings;
        private readonly IContentTypeProvider _contentTypeProvider;
        private readonly IEmailSender _emailSender;
        private const int BufferSize = 81920; // 80 KB buffer for optimal streaming performance

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

        /// <summary>
        /// Downloads a file with support for resume (range requests) and chunked transfer.
        /// Supports HTTP 206 Partial Content for efficient downloads and resume capability.
        /// 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>
        /// <returns>File stream with appropriate headers for resumable downloads</returns>
        /// <response code="200">Full file content</response>
        /// <response code="206">Partial file content (range request)</response>
        /// <response code="404">File not found</response>
        /// <response code="416">Requested range not satisfiable</response>
        [HttpGet("{fileName?}")]
        [SwaggerOperation(Summary = "Download file", Description = "Downloads a file with support for full and ranged (resumable) transfers.")]
        [SwaggerResponse(StatusCodes.Status200OK, "Full file content returned")]
        [SwaggerResponse(StatusCodes.Status206PartialContent, "Partial file content returned for a range request")]
        [SwaggerResponse(StatusCodes.Status400BadRequest, "No file name provided and 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)
        {
            try
            {
                // Use default file name from settings if not provided
                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);
                }

                // Get the file storage path (relative to solution folder)
                var fileStoragePath = _fileStorageSettings.Path;

                // If path is not absolute, make it relative to the solution root
                if (!Path.IsPathRooted(fileStoragePath))
                {
                    var solutionRoot = Directory.GetCurrentDirectory();
                    // Go up from api folder to solution root if needed
                    if (solutionRoot.EndsWith("api", StringComparison.OrdinalIgnoreCase))
                    {
                        solutionRoot = Directory.GetParent(solutionRoot)?.FullName ?? solutionRoot;
                    }
                    fileStoragePath = Path.Combine(solutionRoot, fileStoragePath);
                }

                // Sanitize fileName to prevent directory traversal attacks
                var sanitizedFileName = Path.GetFileName(fileName);
                var filePath = Path.Combine(fileStoragePath, sanitizedFileName);

                // Verify file exists
                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 fileInfo = new FileInfo(filePath);
                var fileLength = fileInfo.Length;

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

                // Send email notification asynchronously (fire and forget with error handling)
                // This is done before streaming to ensure notification is sent for both full and range downloads
                var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
                _ = 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);
                    }
                });

                // Check if this is a range request
                var rangeHeader = Request.Headers[HeaderNames.Range].ToString();

                if (!string.IsNullOrEmpty(rangeHeader))
                {
                    return await HandleRangeRequest(filePath, fileLength, contentType, rangeHeader, sanitizedFileName);
                }

                // Full file download
                _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
            {
                // Parse range header (format: "bytes=start-end")
                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]);
                }

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

                // Adjust end byte if it exceeds file length
                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);

                // Open file stream and seek to start position
                var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, useAsync: true);
                stream.Seek(startByte, SeekOrigin.Begin);

                // Set response headers for partial content
                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;

                // Stream the requested range
                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; // End of stream
                }

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

            await destination.FlushAsync();
        }
    }
}
