using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using CalledIt.DataAccess;
using CalledIt.DataAccess.Entities;
using CalledIt.Domain;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Npgsql;

namespace CalledIt.Seeder;

public static class TestDataSeeder
{
    private static readonly PasswordHasher<UserEntity> _hasher = new();

    private static string ToLogoSlug(string name)
    {
        // Decompose characters so diacritics become separate combining marks (ç → c + ̧)
        var normalized = name.Normalize(NormalizationForm.FormD);
        var sb = new StringBuilder(normalized.Length);
        foreach (var c in normalized)
        {
            var category = CharUnicodeInfo.GetUnicodeCategory(c);
            if (category == UnicodeCategory.NonSpacingMark)
            {
                continue; // strip diacritics (ç cedilla, ô circumflex, etc.)
            }

            sb.Append(c);
        }

        return sb.ToString()
            .Replace('Ø', 'O')          // ø/Ø don't decompose via FormD, replace explicitly
            .Replace('ø', 'o')
            .Replace('/', ' ')          // slash treated as word separator
            .Replace('\'', '-')         // apostrophe → hyphen (Côte d'Ivoire → cote-d-ivoire)
            .Replace('\u2019', '-')     // right single quotation mark
            .ToLowerInvariant()
            .Replace(' ', '-');
    }

    // Copies a local logo file to targetPath. Returns the filename or null if not found.
    private static string? CopyLocalLogo(string name, string logosDir, string targetPath)
    {
        var slug = ToLogoSlug(name);
        var sourcePath = Path.Combine(logosDir, slug + ".svg");

        if (!File.Exists(sourcePath))
        {
            // Retry with "FC" stripped (e.g. "FC Barcelona" → "barcelona", "Manchester City FC" → "manchester-city")
            var nameWithoutFc = Regex.Replace(name, @"\bfc\b", "", RegexOptions.IgnoreCase).Trim();
            nameWithoutFc = Regex.Replace(nameWithoutFc, @"\s+", " ").Trim();

            if (nameWithoutFc != name)
            {
                var fallbackSlug = ToLogoSlug(nameWithoutFc);
                var fallbackPath = Path.Combine(logosDir, fallbackSlug + ".svg");
                if (File.Exists(fallbackPath))
                {
                    File.Copy(fallbackPath, targetPath, overwrite: true);
                    return Path.GetFileName(targetPath);
                }
            }

            Console.WriteLine($"  [WARN] No logo found for '{name}' (expected: {slug}.svg)");
            return null;
        }

        File.Copy(sourcePath, targetPath, overwrite: true);
        return Path.GetFileName(targetPath);
    }

    public static async Task SeedAsync(CalledItDbContext db, IEnumerable<IMatchDataSource> matchDataSources, string uploadsBasePath, string logosDir, string usersJsonPath, bool seedUsers = true, int? seedUserCount = null)
    {
        Dictionary<string, UserEntity> users;
        if (seedUsers)
        {
            users = await SeedUsersAsync(db, usersJsonPath, seedUserCount);
        }
        else
        {
            users = [];
            Console.WriteLine("  Users                          skipped (SEED_USERS=false)");
        }

        foreach (var source in matchDataSources)
        {
            await SeedFromDataSourceAsync(db, source, users, uploadsBasePath, logosDir);
        }
    }

    // -------------------------------------------------------------------------
    // Users
    // -------------------------------------------------------------------------

    private record UserSpec(string Username, bool IsAdmin);

    private static async Task<Dictionary<string, UserEntity>> SeedUsersAsync(CalledItDbContext db, string usersJsonPath, int? userCount = null)
    {
        var json = await File.ReadAllTextAsync(usersJsonPath);
        var serializeroptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
        var fromJson = JsonSerializer.Deserialize<UserSpec[]>(json, serializeroptions)
            ?? throw new InvalidOperationException($"Failed to deserialize {usersJsonPath}");

        UserSpec[] specs;
        if (userCount.HasValue)
        {
            var admins = fromJson.Where(u => u.IsAdmin);
            var players = fromJson.Where(u => !u.IsAdmin).Take(userCount.Value);
            specs = [.. admins, .. players];
        }
        else
        {
            specs = fromJson;
        }

        var existing = await db.Users.ToDictionaryAsync(u => u.Username);
        int created = 0;

        foreach (var spec in specs)
        {
            if (existing.ContainsKey(spec.Username))
            {
                continue;
            }

            var user = new UserEntity
            {
                Username = spec.Username,
                Email = $"{spec.Username}@calledit.test",
                IsAdmin = spec.IsAdmin,
                PasswordHash = string.Empty,
            };
            var password = spec.IsAdmin ? "AdminPassword1" : "TestPassword1";
            user.PasswordHash = _hasher.HashPassword(user, password);
            db.Users.Add(user);

            try
            {
                await db.SaveChangesAsync();
                existing[spec.Username] = user;
                created++;
            }
            catch (DbUpdateException ex)
                when (ex.InnerException is PostgresException { SqlState: "23505" })
            {
                db.Entry(user).State = EntityState.Detached;
            }
        }

        existing = await db.Users.ToDictionaryAsync(u => u.Username);

        PrintLine("Users", created, specs.Length - created);
        return existing;
    }

    // -------------------------------------------------------------------------
    // Competition + Teams + Matches from data source
    // -------------------------------------------------------------------------

    private static async Task SeedFromDataSourceAsync(
        CalledItDbContext db,
        IMatchDataSource dataSource,
        Dictionary<string, UserEntity> users,
        string uploadsBasePath,
        string logosDir)
    {
        Console.WriteLine("  Loading match data...");
        var data = await dataSource.LoadAsync();

        var comp = await SeedCompetitionAsync(db, data, uploadsBasePath, logosDir);
        var teams = await SeedTeamsAsync(db, data.Matches, uploadsBasePath, logosDir);
        var players = users.Where(u => !u.Value.IsAdmin).Select(u => u.Value).ToArray();
        await SeedMatchesWithBetsAsync(db, comp, teams, players, data.Matches);
        await SeedGroupsAsync(db, comp, players);
    }

    private static async Task<CompetitionEntity> SeedCompetitionAsync(
        CalledItDbContext db,
        FootballDataResponse data,
        string uploadsBasePath,
        string logosDir)
    {
        var startDate = DateTime.SpecifyKind(DateTime.Parse(data.ResultSet.First), DateTimeKind.Utc);
        var endDate = DateTime.SpecifyKind(DateTime.Parse(data.ResultSet.Last), DateTimeKind.Utc);
        var name = $"{data.Competition.Name} {startDate.Year}";

        var existing = await db.Competitions.FirstOrDefaultAsync(c => c.Name == name);
        if (existing is not null)
        {
            PrintLine("Competition", 0, 1);
            return existing;
        }

        var comp = new CompetitionEntity
        {
            Name = name,
            StartDate = startDate,
            EndDate = endDate,
            Description = name,
        };
        db.Competitions.Add(comp);
        await db.SaveChangesAsync();

        var competitionsDir = Path.Combine(uploadsBasePath, "competitions");
        Directory.CreateDirectory(competitionsDir);
        var targetPath = Path.Combine(competitionsDir, $"competition-{comp.Id}.svg");
        comp.LogoFileName = CopyLocalLogo(name, logosDir, targetPath);
        await db.SaveChangesAsync();

        PrintLine("Competition", 1, 0);
        return comp;
    }

    private static async Task<Dictionary<string, TeamEntity>> SeedTeamsAsync(
        CalledItDbContext db,
        FootballDataMatch[] matches,
        string uploadsBasePath,
        string logosDir)
    {
        var teamNames = matches
            .SelectMany(m => new[] { m.HomeTeam.Name, m.AwayTeam.Name })
            .Where(n => n is not null)
            .Select(n => n!)
            .Distinct()
            .ToHashSet();

        var existing = await db.Teams.ToDictionaryAsync(t => t.Name);
        int created = 0;

        foreach (var name in teamNames)
        {
            if (existing.ContainsKey(name))
            {
                continue;
            }

            var team = new TeamEntity { Name = name };
            db.Teams.Add(team);
            existing[name] = team;
            created++;
        }

        if (created > 0)
        {
            await db.SaveChangesAsync();
        }

        var teamsDir = Path.Combine(uploadsBasePath, "teams");
        Directory.CreateDirectory(teamsDir);

        foreach (var (name, team) in existing)
        {
            if (team.LogoFileName != null)
            {
                continue;
            }

            var targetPath = Path.Combine(teamsDir, $"team-{team.Id}.svg");
            team.LogoFileName = CopyLocalLogo(name, logosDir, targetPath);
        }

        await db.SaveChangesAsync();
        existing = await db.Teams.ToDictionaryAsync(t => t.Name);

        PrintLine("Teams", created, teamNames.Count - created);
        return existing;
    }

    private static async Task SeedMatchesWithBetsAsync(
        CalledItDbContext db,
        CompetitionEntity comp,
        Dictionary<string, TeamEntity> teams,
        UserEntity[] players,
        FootballDataMatch[] matchData)
    {
        var existingMatches = await db.Matches
            .Where(m => m.CompetitionId == comp.Id)
            .ToListAsync();

        var existingMatchSet = existingMatches
            .Select(m => (m.HomeTeamId, m.AwayTeamId, m.Round))
            .ToHashSet();

        int matchesCreated = 0, matchesSkipped = 0;
        var newMatches = new List<MatchEntity>();

        foreach (var m in matchData)
        {
            if (m.HomeTeam.Name is null || m.AwayTeam.Name is null)
            {
                matchesSkipped++;
                continue;
            }

            if (!teams.TryGetValue(m.HomeTeam.Name, out var homeTeam) ||
                !teams.TryGetValue(m.AwayTeam.Name, out var awayTeam))
            {
                Console.WriteLine($"  [WARN] Team not found: {m.HomeTeam.Name} or {m.AwayTeam.Name}, skipping.");
                matchesSkipped++;
                continue;
            }

            var round = MapStageToRound(m.Stage, m.Group);

            if (existingMatchSet.Contains((homeTeam.Id, awayTeam.Id, round)))
            {
                matchesSkipped++;
                continue;
            }

            var match = new MatchEntity
            {
                CompetitionId = comp.Id,
                HomeTeamId = homeTeam.Id,
                AwayTeamId = awayTeam.Id,
                MatchDate = DateTime.Parse(m.UtcDate).ToUniversalTime(),
                Round = round,
                HomeScore = m.Score.FullTime.Home,
                AwayScore = m.Score.FullTime.Away,
            };

            db.Matches.Add(match);
            newMatches.Add(match);
            matchesCreated++;
        }

        if (matchesCreated > 0)
        {
            await db.SaveChangesAsync();
        }

        PrintLine($"  Matches", matchesCreated, matchesSkipped);

        if (players.Length == 0)
        {
            return;
        }

        var allMatches = await db.Matches
            .Where(m => m.CompetitionId == comp.Id)
            .ToListAsync();

        var matchIds = allMatches.Select(m => m.Id).ToHashSet();
        var existingBetSet = (await db.Bets
            .Where(b => matchIds.Contains(b.MatchId))
            .Select(b => new { b.UserId, b.MatchId })
            .ToListAsync())
            .Select(b => (b.UserId, b.MatchId))
            .ToHashSet();

        int betsCreated = 0, betsSkipped = 0;

        for (int pi = 0; pi < players.Length; pi++)
        {
            var player = players[pi];
            for (int mi = 0; mi < allMatches.Count; mi++)
            {
                var match = allMatches[mi];
                if (existingBetSet.Contains((player.Id, match.Id)))
                {
                    betsSkipped++;
                    continue;
                }

                int betHome = (pi + mi) % 4;
                int betAway = (pi * 2 + mi) % 3;
                int? points = null;

                if (match.HomeScore.HasValue && match.AwayScore.HasValue)
                {
                    points = BetScoringCalculator.Calculate(betHome, betAway, match.HomeScore.Value, match.AwayScore.Value);
                }

                db.Bets.Add(new BetEntity
                {
                    UserId = player.Id,
                    MatchId = match.Id,
                    HomeScore = betHome,
                    AwayScore = betAway,
                    Points = points,
                });
                betsCreated++;
            }
        }

        if (betsCreated > 0)
        {
            await db.SaveChangesAsync();
        }

        PrintLine($"  Bets", betsCreated, betsSkipped);

        await UpsertCompetitionStatsAsync(db, comp.Id, matchIds);
    }

    private static async Task UpsertCompetitionStatsAsync(CalledItDbContext db, int competitionId, HashSet<int> matchIds)
    {
        var computed = await db.Bets
            .Where(b => matchIds.Contains(b.MatchId))
            .GroupBy(b => b.UserId)
            .Select(g => new
            {
                UserId = g.Key,
                Username = g.First().User.Username,
                TotalPoints = g.Sum(b => b.Points ?? 0),
                BetsPlaced = g.Count(),
                BetsEvaluated = g.Count(b => b.Points != null),
            })
            .ToListAsync();

        var existingStats = await db.UserCompetitionStats
            .Where(s => s.CompetitionId == competitionId)
            .ToDictionaryAsync(s => s.UserId);

        foreach (var row in computed)
        {
            if (existingStats.TryGetValue(row.UserId, out var stats))
            {
                stats.Username = row.Username;
                stats.TotalPoints = row.TotalPoints;
                stats.BetsPlaced = row.BetsPlaced;
                stats.BetsEvaluated = row.BetsEvaluated;
            }
            else
            {
                db.UserCompetitionStats.Add(entity: new UserCompetitionStatsEntity
                {
                    UserId = row.UserId,
                    CompetitionId = competitionId,
                    Username = row.Username,
                    TotalPoints = row.TotalPoints,
                    BetsPlaced = row.BetsPlaced,
                    BetsEvaluated = row.BetsEvaluated,
                });
            }
        }

        await db.SaveChangesAsync();
        PrintLine($"  Stats", computed.Count, 0);
    }

    // -------------------------------------------------------------------------
    // Groups
    // -------------------------------------------------------------------------

    private static async Task SeedGroupsAsync(
        CalledItDbContext db,
        CompetitionEntity comp,
        UserEntity[] players)
    {
        if (players.Length == 0)
        {
            return;
        }

        var seedCodes = new[] { "SEEDGRPA", "SEEDGRPB" };
        var seedNames = new[] { "Group Alpha", "Group Beta" };

        var existingGroups = await db.Groups
            .Where(g => seedCodes.Contains(g.InviteCode) && g.CompetitionId == comp.Id)
            .ToListAsync();

        var existingByCode = existingGroups.ToDictionary(g => g.InviteCode);
        var newGroups = new List<GroupEntity>();

        for (int i = 0; i < seedCodes.Length; i++)
        {
            if (existingByCode.ContainsKey(seedCodes[i]))
            {
                continue;
            }

            var group = new GroupEntity
            {
                Name = seedNames[i],
                CompetitionId = comp.Id,
                CreatedByUserId = players[0].Id,
                InviteCode = seedCodes[i],
                CreatedAt = DateTime.UtcNow,
            };
            db.Groups.Add(group);
            newGroups.Add(group);
        }

        if (newGroups.Count > 0)
        {
            await db.SaveChangesAsync();
        }

        var allGroups = existingGroups.Concat(newGroups).ToList();
        var groupIds = allGroups.Select(g => g.Id).ToList();

        var existingMemberKeys = (await db.GroupMembers
            .Where(m => groupIds.Contains(m.GroupId))
            .Select(m => new { m.GroupId, m.UserId })
            .ToListAsync())
            .Select(m => (m.GroupId, m.UserId))
            .ToHashSet();

        int created = 0, skipped = 0;

        foreach (var group in allGroups)
        {
            foreach (var player in players)
            {
                if (existingMemberKeys.Contains((group.Id, player.Id)))
                {
                    skipped++;
                    continue;
                }

                db.GroupMembers.Add(new GroupMemberEntity
                {
                    GroupId = group.Id,
                    UserId = player.Id,
                    JoinedAt = DateTime.UtcNow,
                });
                created++;
            }
        }

        if (created > 0)
        {
            await db.SaveChangesAsync();
        }

        PrintLine("  Groups", newGroups.Count, existingGroups.Count);
        PrintLine("  Group members", created, skipped);
    }

    // -------------------------------------------------------------------------
    // Stage mapping
    // -------------------------------------------------------------------------

    private static string MapStageToRound(string stage, string? group) => stage switch
    {
        "GROUP_STAGE"    => group is null ? "Group Stage" : $"Group {group.Replace("GROUP_", "")}",
        "LAST_32"        => "Round of 32",
        "LAST_16"        => "Round of 16",
        "QUARTER_FINALS" => "Quarter-Finals",
        "SEMI_FINALS"    => "Semi-Finals",
        "THIRD_PLACE"    => "Third Place",
        "FINAL"          => "Final",
        _                => stage,
    };

    // -------------------------------------------------------------------------
    // Output helper
    // -------------------------------------------------------------------------

    private static void PrintLine(string label, int created, int skipped)
    {
        var status = created > 0 || skipped > 0
            ? $"{created,4} created, {skipped,4} skipped"
            : "  nothing to do";
        Console.WriteLine($"  {label,-30} {status}");
    }
}
