using System;
using System.Collections.Generic;
using System.Data.Common;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Microsoft.Data.Sqlite;
using SharpRss.Core.Cache;
using SharpRss.Models;
using ToolQit;
using ToolQit.Logging;

namespace SharpRss
{
    public class ItemFetchState
    {
        public string[]? SyndicationUrls { get; internal set; }
        public int TakeAmount { get; internal set; } = 20;
        public int TakenCount { get; internal set; }
        public HashSet<SyndicationItemModel> Items { get; internal set; } = new HashSet<SyndicationItemModel>();
    }
    internal static class DbAccess
    {
        private static readonly ILog _log = LogManager.CreateLogger(typeof(DbAccess));
        private static readonly string ConnectionString = $"Data Source={Path.Combine(Environment.CurrentDirectory, "sharp_rss.sqlite")};";
        private static bool _isInitialized;

        public static async Task SetSyndicationAsync(SyndicationContainer synContainer)
        {
            if (synContainer.Category != null)
            {
                CategoryModel? catModel = await SetCategoryAsync(synContainer.Category);
                if (catModel != null)
                    synContainer.SyndicationModel.CategoryId = catModel.Id;
            }
            if (synContainer.SyndicationModel != null)
                await SetSyndicationAsync(synContainer.SyndicationModel);
            if (synContainer.SyndicationItems != null && synContainer.SyndicationItems.Any())
                await SetSyndicationItemsAsync(synContainer.SyndicationItems);
        }

        public static async Task<HashSet<CategoryModel>> GetCategoriesAsync()
        {
            HashSet<CategoryModel> categories = new HashSet<CategoryModel>();
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            dbc.Open();
            await using DbDataReader reader = await dbc.ExecuteReaderAsync("SELECT * FROM category");
            while (await reader.ReadAsync())
            {
                CategoryModel categoryModel = new CategoryModel()
                {
                    Id = reader["id"].ToString(),
                    Name = reader["name"].ToString(),
                    HexColor = reader["hex_color"].ToString(),
                    Icon = reader["icon"].ToString(),
                    LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString()))
                };
                categories.Add(categoryModel);
            }
            return categories;
        }
        public static async Task<CategoryModel?> SetCategoryAsync(CategoryModel category)
        {
            CategoryModel? modelReturn = null;
            if (category == null) return modelReturn;
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            dbc.Open();
            int affected = await dbc.ExecuteAsync("INSERT OR REPLACE INTO category (id, name, hex_color, icon, last_updated) VALUES (IFNULL((SELECT id FROM category WHERE name=@Name), @Id), @Name, @HexColor, @Icon, @LastUpdated)",
                new { Id = category.Id, Name = category.Name, HexColor = category.HexColor, Icon = category.Icon, LastUpdated = category.LastUpdated.ToUnixTimeMilliseconds() });
            if (affected <= 0) return modelReturn;
            var catModel = await GetCategoriesAsync();
            modelReturn = catModel.Where(x => x.Name == category.Name).ToHashSet().FirstOrDefault() ?? null;
            return modelReturn;
        }
        public static async Task<bool> DeleteCategory(CategoryModel category)
        {
            if (category == null) return false;
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            dbc.Open();
            int affected = await dbc.ExecuteAsync("DELETE FROM category WHERE id=@Id; UPDATE syndication SET category_id=NULL WHERE category_id=@Id",new { category.Id });
            return affected > 0;
        }

        public static async Task SetSyndicationAsync(SyndicationModel syndication)
        {
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            dbc.Open();
            int affected = await dbc.ExecuteAsync(@"INSERT OR REPLACE INTO syndication
                (encoded_url,
                title,
                category_id,
                syndication_type,
                version,
                description,
                language,
                copyright,
                last_updated,
                publication_date,
                syn_updated_date,
                categories,
                image_url)
                VALUES (
                @EncodedUrl,
                @Title,
                @CategoryId,
                @SyndicationType,
                @Version,
                @Description,
                @Language,
                @Copyright,
                @LastUpdated,
                @PublicationDate,
                @SynUpdatedDate,
                @Categories,
                @ImageUrl)",
                new
                {
                    EncodedUrl = syndication.EncodedUrl,
                    Title = syndication.Title ?? string.Empty,
                    CategoryId = syndication.CategoryId ?? string.Empty,
                    SyndicationType = syndication.SyndicationType ?? string.Empty,
                    Version = syndication.Version ?? string.Empty,
                    Description = syndication.Description ?? string.Empty,
                    Language = syndication.Language ?? string.Empty,
                    Copyright = syndication.Copyright ?? string.Empty,
                    LastUpdated = syndication.LastUpdated.ToUnixTimeMilliseconds(),
                    PublicationDate = syndication.PublicationDate?.ToUnixTimeMilliseconds() ?? 0,
                    SynUpdatedDate = syndication.SynUpdatedDate?.ToUnixTimeMilliseconds() ?? 0,
                    Categories = syndication.Categories != null && syndication.Categories.Any() ? SyndicationManager.FormatStringArrayToString(syndication.Categories) : string.Empty,
                    ImageUrl = syndication.ImageUrl ?? string.Empty
                });
            if (affected == 0)
                _log.Warning("Failed to add feed: {FeedUrl}", syndication.EncodedUrl);
        }
        public static async Task<HashSet<SyndicationModel>> GetSyndicationsAsync(string[]? categoryIds = null)
        {
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            dbc.Open();
            await TempCache.UpdateCache(CacheFetch.Category);
            HashSet<SyndicationModel> feeds = new HashSet<SyndicationModel>();
            await using DbDataReader reader = await dbc.ExecuteReaderAsync(categoryIds == null ? "SELECT * FROM syndication" : "SELECT * FROM syndication WHERE category_id IN(@CatIds)", new { CatIds = categoryIds });
            while (await reader.ReadAsync())
            {
                SyndicationModel syndicationModel = new SyndicationModel()
                {
                    EncodedUrl = reader["encoded_url"].ToString(),
                    Title = reader["title"].ToString(),
                    CategoryId = reader["category_id"].ToString(),
                    SyndicationType = reader["syndication_type"].ToString(),
                    Version = reader["version"].ToString(),
                    Description = reader["description"].ToString(),
                    Language = reader["language"].ToString(),
                    Copyright = reader["copyright"].ToString(),
                    LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())),
                    PublicationDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["publication_date"].ToString())),
                    SynUpdatedDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["syn_updated_date"].ToString())),
                    Categories = SyndicationManager.StringToStringArray(reader["categories"].ToString()),
                    ImageUrl = reader["image_url"].ToString()
                };
                syndicationModel.Category = TempCache.GetCategory(syndicationModel.CategoryId);
                syndicationModel.ItemCount = await dbc.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM syndication_item WHERE encoded_syndication_url=@EncodedFeedUrl", new { EncodedFeedUrl = syndicationModel.EncodedUrl });
                feeds.Add(syndicationModel);
            }
            return feeds;
        }
        public static async Task<bool> DeleteSyndicationAsync(SyndicationModel syndication, bool deleteItems = false)
        {
            if (syndication == null) return false;
            _log.Information("Removing syndication...");
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            int affected = await dbc.ExecuteAsync("DELETE FROM syndication WHERE encoded_url=@EncodedUrl", new { EncodedUrl = syndication.EncodedUrl });
            if (affected > 0 && deleteItems)
                await dbc.ExecuteAsync("DELETE FROM syndication_item WHERE encoded_syndication_url=@EncodedUrl", new { EncodedUrl = syndication.EncodedUrl });
            return affected > 0;
        }
        
        public static async Task SetSyndicationItemsAsync(HashSet<SyndicationItemModel> items)
        {
            _log.Information("Inserting syndication items...");
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            dbc.Open();
            int totalAffected = 0;
            await using SqliteTransaction dbTransaction = dbc.BeginTransaction();
            foreach (SyndicationItemModel item in items)
            {
                int affected = await dbc.ExecuteAsync(@"INSERT OR REPLACE INTO syndication_item (link, encoded_syndication_url, read, type, title, description, last_updated, item_updated_date, publishing_date, authors, categories, content, comments_url)
                    VALUES (@Link, @EncodedSyndicationUrl, @Read, @Type, @Title, @Description, @LastUpdated, @ItemUpdatedDate, @PublishingDate, @Authors, @Categories, @Content, @CommentsUrl)",
                    transaction: dbTransaction,
                    param: new
                    {
                        Link = item.Link ?? string.Empty,
                        EncodedSyndicationUrl = item.EncodedSyndicationUrl ?? string.Empty,
                        Read = item.Read.ToString(),
                        Type = item.Type ?? string.Empty,
                        Title = item.Title ?? string.Empty,
                        Description = item.Description ?? string.Empty,
                        LastUpdated = item.LastUpdated.ToUnixTimeMilliseconds(),
                        ItemUpdatedDate = item.ItemUpdatedDate?.ToUnixTimeMilliseconds() ?? 0,
                        PublishingDate = item.PublishingDate?.ToUnixTimeMilliseconds() ?? 0,
                        Authors = item.Authors != null && item.Authors.Any() ? SyndicationManager.FormatStringArrayToString(item.Authors) : string.Empty,
                        Categories = item.Categories != null && item.Categories.Any() ? SyndicationManager.FormatStringArrayToString(item.Categories) : string.Empty,
                        Content = item.Content ?? string.Empty,
                        CommentsUrl = item.CommentsUrl ?? string.Empty
                    });
                totalAffected += affected;
            }
            dbTransaction.Commit();
        }

        

        public static async Task<bool> GetSyndicationItemsAsync(ItemFetchState state)
        {
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            dbc.Open();
            HashSet<SyndicationItemModel> items = new HashSet<SyndicationItemModel>();
            await TempCache.UpdateCache(CacheFetch.Syndication);
            await using DbDataReader reader = await dbc.ExecuteReaderAsync(state.SyndicationUrls == null ? "SELECT * FROM syndication_item" : $"SELECT * FROM syndication_item WHERE encoded_syndication_url IN({FormatParametersFromArray(state.SyndicationUrls)})");
            //NOTE(dbg): While debugging this part of the function the code below (Reader.ReadAsync) will return 0, because the debugger already reads the items from the reader.
            while (await reader.ReadAsync())
            {
                SyndicationItemModel syndicationItemModel = new SyndicationItemModel()
                {
                    Link = reader["link"].ToString(),
                    EncodedSyndicationUrl = reader["encoded_syndication_url"].ToString(),
                    Read = bool.Parse(reader["read"].ToString()),
                    Type = reader["type"].ToString(),
                    Title = reader["title"].ToString(),
                    Description = reader["description"].ToString(),
                    LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())),
                    ItemUpdatedDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["item_updated_date"].ToString())),
                    PublishingDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["publishing_date"].ToString())),
                    Authors = SyndicationManager.StringToStringArray(reader["authors"].ToString()),
                    Categories = SyndicationManager.StringToStringArray(reader["categories"].ToString()),
                    Content = reader["content"].ToString(),
                    CommentsUrl = reader["comments_url"].ToString()
                };
                syndicationItemModel.SyndicationParent = TempCache.GetSyndication(syndicationItemModel.EncodedSyndicationUrl) ?? new SyndicationModel(); // The new syndication should never be initialized, if this hits then the date is not valid for some reason!!!
                items.Add(syndicationItemModel);
            }
            _log.Information("Fetching feed items resulted: {ItemCount} item(s)", items.Count);
            state.Items = items;
            return true;
        }
        public static async void Initialize()
        {
            if (_isInitialized) return;
            _log.Verbose("Checking database...");
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            dbc.Open();
            _log.Information("Checking table: {Table}", "category");
            await dbc.ExecuteAsync("CREATE TABLE IF NOT EXISTS category (id STRING PRIMARY KEY, name STRING NOT NULL, hex_color STRING NOT NULL, icon STRING, last_updated INT)");

            _log.Information("Checking table: {Table}", "syndication");
            await dbc.ExecuteAsync("CREATE TABLE IF NOT EXISTS syndication (encoded_url STRING PRIMARY KEY, title STRING, category_id STRING, syndication_type STRING, version STRING, description STRING, language STRING, copyright STRING, last_updated INT, publication_date INT, syn_updated_date INT, categories STRING, image_url STRING)");

            _log.Information("Checking table: {Table}", "syndication_item");
            await dbc.ExecuteAsync("CREATE TABLE IF NOT EXISTS syndication_item (link STRING PRIMARY KEY, encoded_syndication_url STRING, read STRING, type STRING, title STRING, description STRING, last_updated INT, item_updated_date INT, publishing_date INT, authors STRING, categories STRING, content STRING, comments_url STRING)");
            _log.Information("Checking database done!");
            _isInitialized = true;
        }
        
        private static string FormatParametersFromArray(string[] dbParams)
        {
            string[] formatted = dbParams.Select(s => $"'{s}'").ToArray();
            return string.Join(", ", formatted);
        }
        /*public static async Task<int> GetSyndicationCountAsync(string[] encodedSyndicationUrls)
        {
            await using SqliteConnection dbc = new SqliteConnection(ConnectionString);
            dbc.Open();
            return await dbc.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM syndication WHERE encoded_url IN(@Urls)");
        }*/
    }
}