using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Npgsql;
using PhoenixLib.Logging;
using WingsAPI.Data.GameData;
using WingsEmu.DTOs.BCards;
using WingsEmu.DTOs.Skills;

namespace Plugin.ResourceLoader.Services
{
    public class ResourceFilesPostgresSyncService : IHostedService
    {
        private readonly ResourceLoadingConfiguration _configuration;
        private readonly IResourceLoader<SkillDTO> _skillLoader;

        public ResourceFilesPostgresSyncService(ResourceLoadingConfiguration configuration, IResourceLoader<SkillDTO> skillLoader)
        {
            _configuration = configuration;
            _skillLoader = skillLoader;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            if (string.Equals(Environment.GetEnvironmentVariable("PARSER_DB_SYNC"), "false", StringComparison.OrdinalIgnoreCase))
            {
                return Task.CompletedTask;
            }

            try
            {
                string host = Environment.GetEnvironmentVariable("DATABASE_IP")
                              ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_IP")
                              ?? "127.0.0.1";
                string port = Environment.GetEnvironmentVariable("DATABASE_PORT")
                              ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PORT")
                              ?? "5432";
                string db = Environment.GetEnvironmentVariable("DATABASE_NAME")
                            ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_NAME")
                            ?? "game";
                string user = Environment.GetEnvironmentVariable("DATABASE_USER")
                              ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_USER")
                              ?? "postgres";
                string pass = Environment.GetEnvironmentVariable("DATABASE_PASSWORD")
                              ?? Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PASSWORD")
                              ?? "postgres";

                string root = _configuration.ResourcePaths;
                string datPath = _configuration.GameDataPath;
                string langPath = _configuration.GameLanguagePath;
                string mapsPath = _configuration.GameMapsPath;
                string configScriptsPath = Path.Combine(Directory.GetCurrentDirectory(), "config", "scripts");

                string[] datFiles = Directory.Exists(datPath)
                    ? Directory.GetFiles(datPath, "*", SearchOption.AllDirectories)
                    : Array.Empty<string>();
                string[] langFiles = Directory.Exists(langPath)
                    ? Directory.GetFiles(langPath, "*", SearchOption.AllDirectories)
                    : Array.Empty<string>();
                string[] mapFiles = Directory.Exists(mapsPath)
                    ? Directory.GetFiles(mapsPath, "*", SearchOption.AllDirectories)
                    : Array.Empty<string>();
                string[] scriptFiles = Directory.Exists(configScriptsPath)
                    ? Directory.GetFiles(configScriptsPath, "*", SearchOption.AllDirectories)
                    : Array.Empty<string>();

                using var conn = new NpgsqlConnection($"Host={host};Port={port};Database={db};Username={user};Password={pass}");
                conn.Open();
                using var tx = conn.BeginTransaction();

                using (var cmd = new NpgsqlCommand(@"CREATE TABLE IF NOT EXISTS resource_files (
  id BIGSERIAL PRIMARY KEY,
  category TEXT NOT NULL,
  relative_path TEXT NOT NULL,
  sha256 TEXT NOT NULL,
  content BYTEA NOT NULL,
  size_bytes INT NOT NULL,
  synced_at TIMESTAMP NOT NULL DEFAULT NOW(),
  UNIQUE(category, relative_path)
);
CREATE TABLE IF NOT EXISTS skills (
  id INT PRIMARY KEY,
  name TEXT,
  class INT,
  cast_id INT,
  cast_time INT,
  cooldown INT,
  mp_cost INT,
  cp_cost INT,
  skill_type INT,
  attack_type INT,
  hit_type INT,
  target_type INT,
  range INT,
  aoe_range INT,
  level_minimum INT,
  element INT,
  data_json JSONB,
  synced_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS skill_bcards (
  id BIGSERIAL PRIMARY KEY,
  skill_id INT NOT NULL,
  bcard_type INT,
  bcard_subtype INT,
  first_data INT,
  second_data INT,
  cast_type INT
);
CREATE TABLE IF NOT EXISTS skill_combos (
  id BIGSERIAL PRIMARY KEY,
  skill_id INT NOT NULL,
  hit INT,
  animation INT,
  effect INT
);", conn, tx))
                {
                    cmd.ExecuteNonQuery();
                }

                UpsertFiles(conn, tx, root, "dat", datFiles);
                UpsertFiles(conn, tx, root, "lang", langFiles);
                UpsertFiles(conn, tx, root, "maps", mapFiles);
                UpsertFiles(conn, tx, Directory.GetCurrentDirectory(), "config_scripts", scriptFiles);

                SyncSkills(conn, tx, _skillLoader.LoadAsync().GetAwaiter().GetResult());

                tx.Commit();
                Log.Info($"[PARSER_DB_SYNC] Synced resource_files dat={datFiles.Length} lang={langFiles.Length} maps={mapFiles.Length} scripts={scriptFiles.Length} skills=ok");
            }
            catch (Exception ex)
            {
                Log.Error("[PARSER_DB_SYNC] Failed to sync resource files", ex);
            }

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

        private static void UpsertFiles(NpgsqlConnection conn, NpgsqlTransaction tx, string root, string category, string[] files)
        {
            foreach (string file in files.Where(File.Exists))
            {
                byte[] content = File.ReadAllBytes(file);
                string hash;
                using (SHA256 sha = SHA256.Create())
                {
                    hash = Convert.ToHexString(sha.ComputeHash(content));
                }

                string relative = Path.GetRelativePath(root, file).Replace('\\', '/');

                using var cmd = new NpgsqlCommand(@"INSERT INTO resource_files(category,relative_path,sha256,content,size_bytes,synced_at)
VALUES (@category,@path,@sha,@content,@size,NOW())
ON CONFLICT (category, relative_path)
DO UPDATE SET sha256=EXCLUDED.sha256, content=EXCLUDED.content, size_bytes=EXCLUDED.size_bytes, synced_at=NOW();", conn, tx);
                cmd.Parameters.AddWithValue("category", category);
                cmd.Parameters.AddWithValue("path", relative);
                cmd.Parameters.AddWithValue("sha", hash);
                cmd.Parameters.AddWithValue("content", content);
                cmd.Parameters.AddWithValue("size", content.Length);
                cmd.ExecuteNonQuery();
            }
        }

        private static void SyncSkills(NpgsqlConnection conn, NpgsqlTransaction tx, IReadOnlyList<SkillDTO> skills)
        {
            using (var cmd = new NpgsqlCommand("TRUNCATE TABLE skills, skill_bcards, skill_combos RESTART IDENTITY;", conn, tx))
            {
                cmd.ExecuteNonQuery();
            }

            foreach (SkillDTO s in skills)
            {
                using (var cmd = new NpgsqlCommand(@"INSERT INTO skills(id,name,class,cast_id,cast_time,cooldown,mp_cost,cp_cost,skill_type,attack_type,hit_type,target_type,range,aoe_range,level_minimum,element,data_json,synced_at)
VALUES (@id,@name,@class,@castId,@castTime,@cooldown,@mp,@cp,@stype,@atype,@htype,@ttype,@range,@aoe,@lvl,@element,@json::jsonb,NOW());", conn, tx))
                {
                    cmd.Parameters.AddWithValue("id", s.Id);
                    cmd.Parameters.AddWithValue("name", (object?)s.Name ?? DBNull.Value);
                    cmd.Parameters.AddWithValue("class", s.Class);
                    cmd.Parameters.AddWithValue("castId", s.CastId);
                    cmd.Parameters.AddWithValue("castTime", s.CastTime);
                    cmd.Parameters.AddWithValue("cooldown", s.Cooldown);
                    cmd.Parameters.AddWithValue("mp", s.MpCost);
                    cmd.Parameters.AddWithValue("cp", s.CPCost);
                    cmd.Parameters.AddWithValue("stype", (int)s.SkillType);
                    cmd.Parameters.AddWithValue("atype", (int)s.AttackType);
                    cmd.Parameters.AddWithValue("htype", (int)s.HitType);
                    cmd.Parameters.AddWithValue("ttype", (int)s.TargetType);
                    cmd.Parameters.AddWithValue("range", s.Range);
                    cmd.Parameters.AddWithValue("aoe", s.AoERange);
                    cmd.Parameters.AddWithValue("lvl", s.LevelMinimum);
                    cmd.Parameters.AddWithValue("element", s.Element);
                    cmd.Parameters.AddWithValue("json", System.Text.Json.JsonSerializer.Serialize(s));
                    cmd.ExecuteNonQuery();
                }

                foreach (BCardDTO b in s.BCards ?? Enumerable.Empty<BCardDTO>())
                {
                    using var bcmd = new NpgsqlCommand("INSERT INTO skill_bcards(skill_id,bcard_type,bcard_subtype,first_data,second_data,cast_type) VALUES (@sid,@t,@st,@f,@sec,@c);", conn, tx);
                    bcmd.Parameters.AddWithValue("sid", s.Id);
                    bcmd.Parameters.AddWithValue("t", b.Type);
                    bcmd.Parameters.AddWithValue("st", b.SubType);
                    bcmd.Parameters.AddWithValue("f", b.FirstData);
                    bcmd.Parameters.AddWithValue("sec", b.SecondData);
                    bcmd.Parameters.AddWithValue("c", b.CastType);
                    bcmd.ExecuteNonQuery();
                }

                foreach (ComboDTO c in s.Combos ?? Enumerable.Empty<ComboDTO>())
                {
                    using var ccmd = new NpgsqlCommand("INSERT INTO skill_combos(skill_id,hit,animation,effect) VALUES (@sid,@h,@a,@e);", conn, tx);
                    ccmd.Parameters.AddWithValue("sid", s.Id);
                    ccmd.Parameters.AddWithValue("h", c.Hit);
                    ccmd.Parameters.AddWithValue("a", c.Animation);
                    ccmd.Parameters.AddWithValue("e", c.Effect);
                    ccmd.ExecuteNonQuery();
                }
            }
        }
    }
}
