using System.Security.Claims;
using EchoHub.Core.Constants;
using EchoHub.Core.Contracts;
using EchoHub.Core.Security;
using EchoHub.Core.Services;
using EchoHub.Core.DTOs;
using EchoHub.Core.Models;
using EchoHub.Server.Data;
using EchoHub.Server.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;

namespace EchoHub.Server.Controllers;

[ApiController]
[Route("api/channels")]
[Authorize]
[EnableRateLimiting("general")]
public class ChannelsController : ControllerBase
{
    private readonly IChannelService _channelService;
    private readonly EchoHubDbContext _db;
    private readonly FileStorageService _fileStorage;
    private readonly ImageToAsciiService _asciiService;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IChatService _chatService;
    private readonly IMessageEncryptionService _encryption;

    public ChannelsController(
        IChannelService channelService,
        EchoHubDbContext db,
        FileStorageService fileStorage,
        ImageToAsciiService asciiService,
        IHttpClientFactory httpClientFactory,
        IChatService chatService,
        IMessageEncryptionService encryption)
    {
        _channelService = channelService;
        _db = db;
        _fileStorage = fileStorage;
        _asciiService = asciiService;
        _httpClientFactory = httpClientFactory;
        _chatService = chatService;
        _encryption = encryption;
    }

    [HttpGet]
    public async Task<IActionResult> GetChannels([FromQuery] int offset = 0, [FromQuery] int limit = 50)
    {
        var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (userIdClaim is null)
            return Unauthorized(new ErrorResponse("Authentication required."));

        offset = Math.Max(0, offset);
        limit = Math.Clamp(limit, 1, 100);

        var result = await _channelService.GetChannelsAsync(Guid.Parse(userIdClaim), offset, limit);
        return Ok(result);
    }

    [HttpPost]
    public async Task<IActionResult> CreateChannel([FromBody] CreateChannelRequest request)
    {
        var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (userIdClaim is null)
            return Unauthorized(new ErrorResponse("Authentication required."));

        var result = await _channelService.CreateChannelAsync(
            Guid.Parse(userIdClaim), request.Name, request.Topic, request.IsPublic, request.Password,
            request.EncryptionSalt, request.WrappedRoomKey);
        if (!result.IsSuccess)
            return MapChannelError(result);

        if (result.Channel!.IsPublic)
            await _chatService.BroadcastChannelUpdatedAsync(result.Channel);

        return Created($"/api/channels/{result.Channel.Name}", result.Channel);
    }

    /// <summary>
    /// Public crypto metadata for a channel: whether it is end-to-end encrypted and the
    /// PBKDF2 salt clients need to derive their join credential. Never returns the
    /// wrapped room key — that is only handed out after a successful join.
    /// </summary>
    [HttpGet("{channel}/crypto")]
    public async Task<IActionResult> GetChannelCrypto(string channel)
    {
        var crypto = await _channelService.GetChannelCryptoAsync(channel);
        if (crypto is null)
            return NotFound(new ErrorResponse($"Channel '{channel}' does not exist."));

        return Ok(crypto);
    }

    /// <summary>
    /// Changes an encrypted channel's passphrase by re-wrapping its room key.
    /// The caller proves knowledge of the old passphrase via the old auth key;
    /// history is never re-encrypted (the room content key does not change).
    /// </summary>
    [HttpPost("{channel}/rekey")]
    public async Task<IActionResult> RekeyChannel(string channel, [FromBody] RekeyChannelRequest request)
    {
        var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (userIdClaim is null)
            return Unauthorized(new ErrorResponse("Authentication required."));

        var result = await _channelService.RekeyChannelAsync(
            Guid.Parse(userIdClaim), channel,
            request.OldPassword, request.NewPassword,
            request.NewEncryptionSalt, request.NewWrappedRoomKey);
        if (!result.IsSuccess)
            return MapChannelError(result);

        return Ok(result.Channel);
    }

    [HttpPut("{channel}/topic")]
    public async Task<IActionResult> UpdateTopic(string channel, [FromBody] UpdateTopicRequest request)
    {
        var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (userIdClaim is null)
            return Unauthorized(new ErrorResponse("Authentication required."));

        var result = await _channelService.UpdateTopicAsync(
            Guid.Parse(userIdClaim), channel, request.Topic);
        if (!result.IsSuccess)
            return MapChannelError(result);

        await _chatService.BroadcastChannelUpdatedAsync(result.Channel!, channel.ToLowerInvariant().Trim());
        return Ok(result.Channel);
    }

    [HttpDelete("{channel}")]
    public async Task<IActionResult> DeleteChannel(string channel)
    {
        var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (userIdClaim is null)
            return Unauthorized(new ErrorResponse("Authentication required."));

        var result = await _channelService.DeleteChannelAsync(Guid.Parse(userIdClaim), channel);
        if (!result.IsSuccess)
            return MapChannelError(result);

        return NoContent();
    }

    /// <summary>
    /// Sends one message carrying optional text (<c>content</c> form field) plus zero or more
    /// file attachments (Discord-style). For non-encrypted channels the server sniffs each file's
    /// kind and renders ASCII previews for images. For end-to-end encrypted channels the client
    /// uploads ciphertext blobs and declares each file's kind (<c>kind</c>) and pre-rendered,
    /// room-encrypted preview (<c>preview</c>), aligned by file order — the server never inspects them.
    /// </summary>
    [HttpPost("{channel}/messages")]
    [EnableRateLimiting("upload")]
    [RequestSizeLimit((long)HubConstants.MaxFileSizeBytes * HubConstants.MaxAttachmentsPerMessage)]
    [RequestFormLimits(MultipartBodyLengthLimit = (long)HubConstants.MaxFileSizeBytes * HubConstants.MaxAttachmentsPerMessage)]
    public async Task<IActionResult> SendMessageWithAttachments(string channel, [FromQuery] string? size = null)
    {
        var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
        var usernameClaim = User.FindFirstValue("username");
        if (userIdClaim is null || usernameClaim is null)
            return Unauthorized(new ErrorResponse("Authentication required."));

        var userId = Guid.Parse(userIdClaim);
        var channelName = channel.ToLowerInvariant().Trim();

        if (!ValidationConstants.ChannelNameRegex().IsMatch(channelName))
            return BadRequest(new ErrorResponse("Invalid channel name format."));

        var channelDto = await _channelService.GetChannelByNameAsync(channelName);
        if (channelDto is null)
            return NotFound(new ErrorResponse($"Channel '{channelName}' does not exist."));

        if (!Request.HasFormContentType)
            return BadRequest(new ErrorResponse("Expected multipart form data."));

        var files = Request.Form.Files;
        if (files.Count == 0)
            return BadRequest(new ErrorResponse("At least one attachment is required. Send plain text over the chat connection."));
        if (files.Count > HubConstants.MaxAttachmentsPerMessage)
            return BadRequest(new ErrorResponse($"A message may carry at most {HubConstants.MaxAttachmentsPerMessage} attachments."));

        var sender = await _db.Users.FindAsync(userId);
        if (sender is not null && sender.IsMuted && (sender.MutedUntil is null || sender.MutedUntil > DateTimeOffset.UtcNow))
            return StatusCode(403, new ErrorResponse("You are muted and cannot send messages."));

        // Caption: plaintext for normal channels, $RC1$ room-ciphertext for encrypted ones.
        // Decrypt() is a pass-through when there is no transport prefix.
        var content = _encryption.Decrypt(Request.Form["content"].ToString());
        var isRoomCiphertext = RoomCrypto.IsRoomCiphertext(content);
        if (!isRoomCiphertext && content.Length > HubConstants.MaxMessageLength)
            return BadRequest(new ErrorResponse($"Message exceeds maximum length of {HubConstants.MaxMessageLength} characters."));

        var declaredKinds = Request.Form["kind"];
        var declaredPreviews = Request.Form["preview"];

        var attachmentEntities = new List<Attachment>();
        var attachmentDtos = new List<AttachmentDto>();

        for (var i = 0; i < files.Count; i++)
        {
            var file = files[i];
            AttachmentKind kind;
            string? previewPlain;
            string fileId;

            if (channelDto.IsEncrypted)
            {
                // Ciphertext blob — trust the client's declared kind + room-encrypted preview.
                // Client sends one kind + preview per file in order; empty preview means none.
                kind = ParseKind(i < declaredKinds.Count ? declaredKinds[i] : null);
                previewPlain = i < declaredPreviews.Count ? declaredPreviews[i] : null;
                if (string.IsNullOrEmpty(previewPlain))
                    previewPlain = null;

                if (file.Length > MaxForKind(kind))
                    return BadRequest(new ErrorResponse($"'{file.FileName}' exceeds the maximum size."));

                using var encryptedStream = file.OpenReadStream();
                (fileId, _) = await _fileStorage.SaveFileAsync(encryptedStream, file.FileName);
            }
            else
            {
                using var stream = file.OpenReadStream();
                var isImage = FileValidationHelper.IsValidImage(stream);
                var isAudio = !isImage && FileValidationHelper.IsAudioFile(file.FileName);
                kind = isImage ? AttachmentKind.Image : isAudio ? AttachmentKind.Audio : AttachmentKind.File;

                if (file.Length > MaxForKind(kind))
                    return BadRequest(new ErrorResponse($"'{file.FileName}' exceeds the maximum size of {MaxForKind(kind) / (1024 * 1024)} MB."));

                string filePath;
                (fileId, filePath) = await _fileStorage.SaveFileAsync(stream, file.FileName);

                if (isImage)
                {
                    var (w, h) = ImageToAsciiService.GetDimensions(size);
                    using var imageStream = System.IO.File.OpenRead(filePath);
                    previewPlain = _asciiService.ConvertToAscii(imageStream, w, h);
                }
                else
                {
                    previewPlain = null;
                }
            }

            var url = $"/api/files/{fileId}";
            attachmentEntities.Add(new Attachment
            {
                Id = Guid.NewGuid(),
                Kind = kind,
                Url = url,
                FileName = file.FileName,
                FileSize = file.Length,
                AsciiPreview = _encryption.EncryptDatabaseEnabled ? _encryption.EncryptNullable(previewPlain) : previewPlain,
            });
            attachmentDtos.Add(new AttachmentDto(kind, url, file.FileName, file.Length,
                _encryption.EncryptNullable(previewPlain)));
        }

        var dbContent = _encryption.EncryptDatabaseEnabled ? _encryption.Encrypt(content) : content;
        var message = new Message
        {
            Id = Guid.NewGuid(),
            Content = dbContent,
            SentAt = DateTimeOffset.UtcNow,
            ChannelId = channelDto.Id,
            SenderUserId = userId,
            SenderUsername = usernameClaim,
            Attachments = attachmentEntities,
        };

        _db.Messages.Add(message);
        await _db.SaveChangesAsync();

        var messageDto = new MessageDto(
            message.Id,
            _encryption.Encrypt(content),
            message.SenderUsername,
            sender?.NicknameColor,
            channelName,
            message.SentAt,
            attachmentDtos);

        await _chatService.BroadcastMessageAsync(channelName, messageDto);

        return Ok(messageDto);
    }

    private static AttachmentKind ParseKind(string? kind) => kind?.ToLowerInvariant() switch
    {
        "image" => AttachmentKind.Image,
        "audio" => AttachmentKind.Audio,
        _ => AttachmentKind.File,
    };

    private static long MaxForKind(AttachmentKind kind) => kind switch
    {
        AttachmentKind.Image => HubConstants.MaxImageSizeBytes,
        AttachmentKind.Audio => HubConstants.MaxAudioFileSizeBytes,
        _ => HubConstants.MaxFileSizeBytes,
    };

    [HttpPost("{channel}/send-url")]
    [EnableRateLimiting("upload")]
    public async Task<IActionResult> SendUrl(string channel, [FromBody] SendUrlRequest request, [FromQuery] string? size = null)
    {
        var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
        var usernameClaim = User.FindFirstValue("username");
        if (userIdClaim is null || usernameClaim is null)
            return Unauthorized(new ErrorResponse("Authentication required."));

        var userId = Guid.Parse(userIdClaim);
        var channelName = channel.ToLowerInvariant().Trim();

        if (!ValidationConstants.ChannelNameRegex().IsMatch(channelName))
            return BadRequest(new ErrorResponse("Invalid channel name format."));

        var channelDto = await _channelService.GetChannelByNameAsync(channelName);
        if (channelDto is null)
            return NotFound(new ErrorResponse($"Channel '{channelName}' does not exist."));

        if (channelDto.IsEncrypted)
            return BadRequest(new ErrorResponse(
                "Sending images by URL is not available in end-to-end encrypted channels — download the image and /send the file instead."));

        if (string.IsNullOrWhiteSpace(request.Url))
            return BadRequest(new ErrorResponse("URL is required."));

        if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var uri)
            || (uri.Scheme != "http" && uri.Scheme != "https"))
            return BadRequest(new ErrorResponse("Invalid URL. Only http and https are supported."));

        // Download image from URL
        byte[] imageBytes;
        string fileName;
        try
        {
            using var client = _httpClientFactory.CreateClient("ImageDownload");
            using var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
            response.EnsureSuccessStatusCode();

            var contentLength = response.Content.Headers.ContentLength;
            if (contentLength > HubConstants.MaxImageSizeBytes)
                return BadRequest(new ErrorResponse($"File size exceeds maximum of {HubConstants.MaxImageSizeBytes / (1024 * 1024)} MB."));

            imageBytes = await response.Content.ReadAsByteArrayAsync();

            if (imageBytes.Length > HubConstants.MaxImageSizeBytes)
                return BadRequest(new ErrorResponse($"File size exceeds maximum of {HubConstants.MaxImageSizeBytes / (1024 * 1024)} MB."));

            fileName = Path.GetFileName(uri.LocalPath);
            if (string.IsNullOrWhiteSpace(fileName) || !fileName.Contains('.'))
            {
                var contentType = response.Content.Headers.ContentType?.MediaType ?? "";
                var ext = contentType switch
                {
                    "image/png" => ".png",
                    "image/jpeg" or "image/jpg" => ".jpg",
                    "image/gif" => ".gif",
                    "image/webp" => ".webp",
                    _ => ".bin"
                };
                fileName = $"download{ext}";
            }
        }
        catch (TaskCanceledException)
        {
            return BadRequest(new ErrorResponse("Download timed out. The URL may be unreachable."));
        }
        catch (HttpRequestException ex)
        {
            return BadRequest(new ErrorResponse($"Failed to download from URL: {ex.Message}"));
        }

        // Validate it's actually an image
        using var memoryStream = new MemoryStream(imageBytes);
        if (!FileValidationHelper.IsValidImage(memoryStream))
            return BadRequest(new ErrorResponse("The URL does not point to a valid image. Supported formats: JPEG, PNG, GIF, WebP."));

        // Save file and convert to ASCII
        var (fileId, filePath) = await _fileStorage.SaveFileAsync(memoryStream, fileName);

        string preview;
        var (w, h) = ImageToAsciiService.GetDimensions(size);
        using (var imageStream = System.IO.File.OpenRead(filePath))
        {
            preview = _asciiService.ConvertToAscii(imageStream, w, h);
        }

        var attachmentUrl = $"/api/files/{fileId}";
        var sender = await _db.Users.FindAsync(userId);

        // A URL-shared image is a message with no caption and one image attachment.
        var attachment = new Attachment
        {
            Id = Guid.NewGuid(),
            Kind = AttachmentKind.Image,
            Url = attachmentUrl,
            FileName = fileName,
            FileSize = imageBytes.Length,
            AsciiPreview = _encryption.EncryptDatabaseEnabled ? _encryption.Encrypt(preview) : preview,
        };

        var message = new Message
        {
            Id = Guid.NewGuid(),
            Content = string.Empty,
            SentAt = DateTimeOffset.UtcNow,
            ChannelId = channelDto.Id,
            SenderUserId = userId,
            SenderUsername = usernameClaim,
            Attachments = [attachment],
        };

        _db.Messages.Add(message);
        await _db.SaveChangesAsync();

        var messageDto = new MessageDto(
            message.Id,
            _encryption.Encrypt(string.Empty),
            message.SenderUsername,
            sender?.NicknameColor,
            channelName,
            message.SentAt,
            [new AttachmentDto(AttachmentKind.Image, attachmentUrl, fileName, imageBytes.Length, _encryption.Encrypt(preview))]);

        await _chatService.BroadcastMessageAsync(channelName, messageDto);

        return Ok(messageDto);
    }

    private IActionResult MapChannelError(ChannelOperationResult result) => result.Error switch
    {
        ChannelError.ValidationFailed => BadRequest(new ErrorResponse(result.ErrorMessage!)),
        ChannelError.AlreadyExists => Conflict(new ErrorResponse(result.ErrorMessage!)),
        ChannelError.NotFound => NotFound(new ErrorResponse(result.ErrorMessage!)),
        ChannelError.Forbidden => StatusCode(403, new ErrorResponse(result.ErrorMessage!)),
        ChannelError.Protected => BadRequest(new ErrorResponse(result.ErrorMessage!)),
        _ => BadRequest(new ErrorResponse(result.ErrorMessage ?? "Unknown error.")),
    };
}
