using System;
using System.IO;
using System.Threading.Tasks;
using System.Xml.Serialization;
using API.Data;
using API.DTOs.OPDS;
using API.DTOs.OPDS.Requests;
using API.DTOs.Progress;
using API.Entities.Enums;
using API.Exceptions;
using API.Extensions;
using API.Middleware;
using API.Services;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MimeTypes;
namespace API.Controllers;
#nullable enable
[AllowAnonymous]
[ServiceFilter(typeof(OpdsActionFilterAttribute))]
[ServiceFilter(typeof(OpdsActiveUserMiddlewareAttribute))]
public class OpdsController : BaseApiController
{
    private readonly IOpdsService _opdsService;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IDownloadService _downloadService;
    private readonly IDirectoryService _directoryService;
    private readonly ICacheService _cacheService;
    private readonly IReaderService _readerService;
    private readonly IAccountService _accountService;
    private readonly ILocalizationService _localizationService;
    private readonly XmlSerializer _xmlOpenSearchSerializer;
    public const string UserId = nameof(UserId);
    public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
        IDirectoryService directoryService, ICacheService cacheService,
        IReaderService readerService, IAccountService accountService,
        ILocalizationService localizationService, IOpdsService opdsService)
    {
        _unitOfWork = unitOfWork;
        _downloadService = downloadService;
        _directoryService = directoryService;
        _cacheService = cacheService;
        _readerService = readerService;
        _accountService = accountService;
        _localizationService = localizationService;
        _opdsService = opdsService;
        _xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
    }
    private int GetUserIdFromContext()
    {
        return (int) HttpContext.Items[UserId]!;
    }
    /// 
    /// Returns the Catalogue for Kavita's OPDS Service
    /// 
    /// 
    /// 
    [HttpPost("{apiKey}")]
    [HttpGet("{apiKey}")]
    [Produces("application/xml")]
    public async Task Get(string apiKey)
    {
        var (baseUrl, prefix) = await GetPrefix();
        var feed = await _opdsService.GetCatalogue(new OpdsCatalogueRequest
        {
            ApiKey = apiKey,
            Prefix = prefix,
            BaseUrl =  baseUrl,
            UserId = GetUserIdFromContext()
        });
        return CreateXmlResult(_opdsService.SerializeXml(feed));
    }
    private async Task> GetPrefix()
    {
        var baseUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl)).Value;
        var prefix = OpdsService.DefaultApiPrefix;
        if (!Configuration.DefaultBaseUrl.Equals(baseUrl, StringComparison.InvariantCultureIgnoreCase))
        {
            // We need to update the Prefix to account for baseUrl
            prefix = baseUrl.TrimEnd('/') + OpdsService.DefaultApiPrefix;
        }
        return new Tuple(baseUrl, prefix);
    }
    /// 
    /// Get the User's Smart Filter series - Supports Pagination
    /// 
    /// 
    [HttpGet("{apiKey}/smart-filters/{filterId}")]
    [Produces("application/xml")]
    public async Task GetSmartFilter(string apiKey, int filterId, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        var userId = GetUserIdFromContext();
        var (baseUrl, prefix) = await GetPrefix();
        var feed = await _opdsService.GetSeriesFromSmartFilter(new OpdsItemsFromEntityIdRequest()
        {
            ApiKey = apiKey,
            Prefix =  prefix,
            BaseUrl = baseUrl,
            EntityId = filterId,
            UserId = userId,
            PageNumber = pageNumber
        });
        return CreateXmlResult(_opdsService.SerializeXml(feed));
    }
    /// 
    /// Get the User's Smart Filters (Dashboard Context) - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/smart-filters")]
    [Produces("application/xml")]
    public async Task GetSmartFilters(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var userId = GetUserIdFromContext();
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetSmartFilters(new OpdsPaginatedCatalogueRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = userId,
                ApiKey = apiKey,
                PageNumber = pageNumber
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Get the User's Libraries - No Pagination Support
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/libraries")]
    [Produces("application/xml")]
    public async Task GetLibraries(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetLibraries(new OpdsPaginatedCatalogueRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Get the User's Want to Read list - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/want-to-read")]
    [Produces("application/xml")]
    public async Task GetWantToRead(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetWantToRead(new OpdsPaginatedCatalogueRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Get all Collections - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/collections")]
    [Produces("application/xml")]
    public async Task GetCollections(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetCollections(new OpdsPaginatedCatalogueRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Get Series for a given Collection - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/collections/{collectionId}")]
    [Produces("application/xml")]
    public async Task GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetSeriesFromCollection(new OpdsItemsFromEntityIdRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber,
                EntityId = collectionId
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Get a User's Reading Lists - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/reading-list")]
    [Produces("application/xml")]
    public async Task GetReadingLists(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetReadingLists(new OpdsPaginatedCatalogueRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Returns individual items (chapters) from Reading List by ID - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/reading-list/{readingListId}")]
    [Produces("application/xml")]
    public async Task GetReadingListItems(int readingListId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetReadingListItems(new OpdsItemsFromEntityIdRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber,
                EntityId = readingListId
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Returns Series from the Library - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/libraries/{libraryId}")]
    [Produces("application/xml")]
    public async Task GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetSeriesFromLibrary(new OpdsItemsFromEntityIdRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber,
                EntityId = libraryId
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Returns Recently Added (Dashboard Feed) - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/recently-added")]
    [Produces("application/xml")]
    public async Task GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetRecentlyAdded(new OpdsPaginatedCatalogueRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber,
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Returns More In a Genre (Dashboard Feed) - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/more-in-genre")]
    [Produces("application/xml")]
    public async Task GetMoreInGenre(string apiKey, [FromQuery] int genreId, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetMoreInGenre(new OpdsItemsFromEntityIdRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber,
                EntityId = genreId
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Get the Recently Updated Series (Dashboard) - Pagination available, total pages will not be filled due to underlying implementation
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/recently-updated")]
    [Produces("application/xml")]
    public async Task GetRecentlyUpdated(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetRecentlyUpdated(new OpdsPaginatedCatalogueRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber,
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Get the On Deck (Dashboard) - Supports Pagination
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/on-deck")]
    [Produces("application/xml")]
    public async Task GetOnDeck(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetOnDeck(new OpdsPaginatedCatalogueRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                PageNumber = pageNumber,
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// OPDS Search endpoint
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/series")]
    [Produces("application/xml")]
    public async Task SearchSeries(string apiKey, [FromQuery] string query)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.Search(new OpdsSearchRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                Query = query,
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    [HttpGet("{apiKey}/search")]
    [Produces("application/xml")]
    public async Task GetSearchDescriptor(string apiKey)
    {
        var userId = GetUserIdFromContext();
        var (_, prefix) = await GetPrefix();
        var feed = new OpenSearchDescription()
        {
            ShortName = await _localizationService.Translate(userId, "search"),
            Description = await _localizationService.Translate(userId, "search-description"),
            Url = new SearchLink()
            {
                Type = FeedLinkType.AtomAcquisition,
                Template = $"{prefix}{apiKey}/series?query=" + "{searchTerms}"
            }
        };
        await using var sm = new StringWriter();
        _xmlOpenSearchSerializer.Serialize(sm, feed);
        return CreateXmlResult(sm.ToString().Replace("utf-16", "utf-8"));
    }
    /// 
    /// Returns the items within a Series (Series Detail)
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/series/{seriesId}")]
    [Produces("application/xml")]
    public async Task GetSeriesDetail(string apiKey, int seriesId)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                EntityId = seriesId
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Returns items for a given Volume
    /// 
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}")]
    [Produces("application/xml")]
    public async Task GetVolume(string apiKey, int seriesId, int volumeId)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetItemsFromVolume(new OpdsItemsFromCompoundEntityIdsRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                SeriesId = seriesId,
                VolumeId = volumeId
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Gets items for a given Chapter
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}")]
    [Produces("application/xml")]
    public async Task GetChapter(string apiKey, int seriesId, int volumeId, int chapterId)
    {
        try
        {
            var (baseUrl, prefix) = await GetPrefix();
            var feed = await _opdsService.GetItemsFromChapter(new OpdsItemsFromCompoundEntityIdsRequest()
            {
                BaseUrl = baseUrl,
                Prefix = prefix,
                UserId = GetUserIdFromContext(),
                ApiKey = apiKey,
                SeriesId = seriesId,
                VolumeId = volumeId,
                ChapterId = chapterId
            });
            return CreateXmlResult(_opdsService.SerializeXml(feed));
        }
        catch (OpdsException ex)
        {
            return BadRequest(ex.Message);
        }
    }
    /// 
    /// Downloads a file (user must have download permission)
    /// 
    /// User's API Key
    /// 
    /// 
    /// 
    /// Not used. Only for Chunky to allow download links
    /// 
    [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}")]
    public async Task DownloadFile(string apiKey, int seriesId, int volumeId, int chapterId, string filename)
    {
        var userId = GetUserIdFromContext();
        var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
        if (!await _accountService.HasDownloadPermission(user))
        {
            return Forbid(await _localizationService.Translate(userId, "download-not-allowed"));
        }
        var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
        var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
        return PhysicalFile(zipFile, contentType, fileDownloadName, true);
    }
    private static ContentResult CreateXmlResult(string xml)
    {
        return new ContentResult
        {
            ContentType = "application/xml",
            Content = xml,
            StatusCode = 200
        };
    }
    /// 
    /// This returns a streamed image following OPDS-PS v1.2
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// Optional parameter. Can pass false and progress saving will be suppressed
    /// 
    [HttpGet("{apiKey}/image")]
    public async Task GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId,
        [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber, [FromQuery] bool saveProgress = true)
    {
        var userId = GetUserIdFromContext();
        if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page"));
        var chapter = await _cacheService.Ensure(chapterId, true);
        if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "cache-file-find"));
        try
        {
            var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber);
            if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
                return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber));
            var content = await _directoryService.ReadFileAsync(path);
            var format = Path.GetExtension(path);
            // Calculates SHA1 Hash for byte[]
            Response.AddCacheHeader(content);
            // Save progress for the user (except Panels, they will use a direct connection)
            var userAgent = Request.Headers.UserAgent.ToString();
            if (!userAgent.StartsWith("Panels", StringComparison.InvariantCultureIgnoreCase) || !saveProgress)
            {
                // Kavita expects 0-N for progress, KOReader doesn't respect the OPDS-PS spec and does some wierd stuff
                // https://github.com/Kareadita/Kavita/pull/4014#issuecomment-3313677492
                var koreaderOffset = 0;
                if (userAgent.StartsWith("Koreader", StringComparison.InvariantCultureIgnoreCase))
                {
                    var totalPages = await _unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId);
                    if (totalPages - pageNumber < 2)
                    {
                        koreaderOffset = 1;
                    }
                }
                await _readerService.SaveReadingProgress(new ProgressDto()
                {
                    ChapterId = chapterId,
                    PageNum = pageNumber + koreaderOffset,
                    SeriesId = seriesId,
                    VolumeId = volumeId,
                    LibraryId =libraryId
                }, userId);
            }
            return File(content, MimeTypeMap.GetMimeType(format));
        }
        catch (Exception)
        {
            _cacheService.CleanupChapters([chapterId]);
            throw;
        }
    }
    [HttpGet("{apiKey}/favicon")]
    [ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Client, NoStore = false)]
    public async Task GetFavicon(string apiKey)
    {
        var userId = GetUserIdFromContext();
        var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico");
        if (files.Length == 0) return BadRequest(await _localizationService.Translate(userId, "favicon-doesnt-exist"));
        var path = files[0];
        var content = await _directoryService.ReadFileAsync(path);
        var format = Path.GetExtension(path);
        return File(content, MimeTypeMap.GetMimeType(format));
    }
}