using Backend.Contracts;
using Backend.Data;
using Backend.Domain;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration["VTSA_POSTGRES"]
    ?? builder.Configuration.GetConnectionString("Postgres");

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddCors(options =>
{
    options.AddPolicy("frontend", policy =>
    {
        policy
            .WithOrigins(
                "http://localhost:5173",
                "http://127.0.0.1:5173",
                "http://localhost:4173",
                "http://127.0.0.1:4173")
            .AllowAnyHeader()
            .AllowAnyMethod();
    });
});

builder.Services.AddDbContext<AwardsDbContext>(options =>
{
    options.UseNpgsql(connectionString);
});

var app = builder.Build();

static string? ReadBearerToken(HttpContext context)
{
    var header = context.Request.Headers.Authorization.ToString();
    return header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
        ? header["Bearer ".Length..].Trim()
        : null;
}

static async Task<UserSession?> ResolveSessionAsync(HttpContext context, AwardsDbContext db)
{
    var token = ReadBearerToken(context);
    if (string.IsNullOrWhiteSpace(token))
    {
        return null;
    }

    var session = await db.UserSessions.FirstOrDefaultAsync(item => item.SessionToken == token && item.IsActive);
    if (session is not null)
    {
        session.LastSeenAt = DateTimeOffset.UtcNow;
        await db.SaveChangesAsync();
    }

    return session;
}

static string ReadClientIp(HttpContext context) =>
    context.Connection.RemoteIpAddress?.ToString() ?? "unknown";

static string ReadUserAgent(HttpContext context)
{
    var value = context.Request.Headers.UserAgent.ToString().Trim();
    return value.Length > 400 ? value[..400] : value;
}

static void AddAuditEntry(
    AwardsDbContext db,
    string adminTwitchUserId,
    string actionType,
    string entityType,
    string entityId,
    string summary,
    object? metadata = null)
{
    db.AdminAuditEntries.Add(new AdminAuditEntry
    {
        AdminTwitchUserId = adminTwitchUserId,
        ActionType = actionType,
        EntityType = entityType,
        EntityId = entityId,
        Summary = summary,
        MetadataJson = JsonSerializer.Serialize(metadata ?? new { }),
        CreatedAt = DateTimeOffset.UtcNow,
    });
}

static async Task AddRiskFlagIfMissingAsync(
    AwardsDbContext db,
    int? seasonId,
    string? twitchUserId,
    string source,
    string type,
    string severity,
    string summary,
    string createdFromIp,
    string userAgent,
    object? metadata = null)
{
    var threshold = DateTimeOffset.UtcNow.AddHours(-6);
    var exists = await db.RiskFlags.AnyAsync(item =>
        item.Status == "open"
        && item.Source == source
        && item.Type == type
        && item.TwitchUserId == twitchUserId
        && item.CreatedFromIp == createdFromIp
        && item.SeasonId == seasonId
        && item.CreatedAt >= threshold);

    if (exists)
    {
        return;
    }

    db.RiskFlags.Add(new RiskFlag
    {
        SeasonId = seasonId,
        TwitchUserId = twitchUserId,
        Source = source,
        Type = type,
        Severity = severity,
        Status = "open",
        Summary = summary,
        CreatedFromIp = createdFromIp,
        UserAgent = userAgent,
        MetadataJson = JsonSerializer.Serialize(metadata ?? new { }),
        CreatedAt = DateTimeOffset.UtcNow,
    });
}

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseCors("frontend");
app.UseHttpsRedirection();

using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AwardsDbContext>();
    if (app.Environment.IsDevelopment())
    {
        try
        {
            db.Database.Migrate();
        }
        catch
        {
            // In local environments without PostgreSQL yet, the API should still boot
            // so frontend work and migration generation can continue independently.
        }
    }

    try
    {
        await SessionBootstrapper.EnsureAsync(db);
        await OperationalTablesBootstrapper.EnsureAsync(db);
    }
    catch
    {
        // If the operational table bootstrap fails, the rest of the API can still start.
    }
}

app.MapPost("/api/auth/dev-login", async (HttpContext context, LoginRequest request, AwardsDbContext db) =>
{
    var createdFromIp = ReadClientIp(context);
    var userAgent = ReadUserAgent(context);
    var session = new UserSession
    {
        Id = Guid.NewGuid(),
        SessionToken = Guid.NewGuid().ToString("N"),
        TwitchUserId = request.TwitchUserId.Trim(),
        DisplayName = request.DisplayName.Trim(),
        Role = string.Equals(request.Role, "admin", StringComparison.OrdinalIgnoreCase) ? "admin" : "viewer",
        CreatedFromIp = createdFromIp,
        UserAgent = userAgent,
        CreatedAt = DateTimeOffset.UtcNow,
        LastSeenAt = DateTimeOffset.UtcNow,
        IsActive = true,
    };

    var recentSessionsFromIp = await db.UserSessions.CountAsync(item =>
        item.CreatedFromIp == createdFromIp
        && item.CreatedAt >= DateTimeOffset.UtcNow.AddMinutes(-15));

    if (recentSessionsFromIp >= 3)
    {
        await AddRiskFlagIfMissingAsync(
            db,
            null,
            session.TwitchUserId,
            "login",
            "rapid_login_ip",
            "medium",
            "Mehrere neue Sessions wurden in kurzer Zeit von derselben IP erzeugt.",
            createdFromIp,
            userAgent,
            new { recentSessionsFromIp });
    }

    db.UserSessions.Add(session);
    await db.SaveChangesAsync();

    return Results.Ok(new AuthSessionDto(
        session.SessionToken,
        session.TwitchUserId,
        session.DisplayName,
        session.Role));
})
.WithName("DevLogin")
.WithOpenApi();

app.MapGet("/api/auth/session", async (HttpContext context, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session is null)
    {
        return Results.Unauthorized();
    }

    return Results.Ok(new AuthSessionDto(
        session.SessionToken,
        session.TwitchUserId,
        session.DisplayName,
        session.Role));
})
.WithName("GetSession")
.WithOpenApi();

app.MapPost("/api/auth/logout", async (HttpContext context, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session is null)
    {
        return Results.Ok(new { loggedOut = true });
    }

    session.IsActive = false;
    await db.SaveChangesAsync();

    return Results.Ok(new { loggedOut = true });
})
.WithName("Logout")
.WithOpenApi();

app.MapGet("/api/health", () => Results.Ok(new { status = "ok" }))
    .WithName("GetHealth")
    .WithOpenApi();

app.MapGet("/api/health/database", async (AwardsDbContext db) =>
{
    try
    {
        var canConnect = await db.Database.CanConnectAsync();
        var pendingMigrations = canConnect
            ? await db.Database.GetPendingMigrationsAsync()
            : Array.Empty<string>();

        return Results.Ok(new
        {
            provider = "postgres",
            canConnect,
            pendingMigrations,
            configuredConnection = new
            {
                source = builder.Configuration["VTSA_POSTGRES"] is not null ? "environment" : "appsettings",
            },
        });
    }
    catch (Exception exception)
    {
        return Results.Ok(new
        {
            provider = "postgres",
            canConnect = false,
            pendingMigrations = Array.Empty<string>(),
            configuredConnection = new
            {
                source = builder.Configuration["VTSA_POSTGRES"] is not null ? "environment" : "appsettings",
            },
            error = exception.Message,
        });
    }
})
.WithName("GetDatabaseHealth")
.WithOpenApi();

app.MapGet("/api/public/overview", async (AwardsDbContext db) =>
{
    var season = await db.Seasons
        .AsNoTracking()
        .Include(item => item.Categories.OrderBy(category => category.SortOrder))
        .Include(item => item.Results)
        .ThenInclude(result => result.Candidate)
        .FirstOrDefaultAsync(item => item.IsCurrent);

    if (season is null)
    {
        return Results.NotFound();
    }

    var response = new OverviewResponse(
        season.Id,
        season.Year,
        season.Name,
        season.ShowDate,
        season.CurrentPhase,
        season.IsCommunityOnly,
        "Twitch",
        new[]
        {
            new TimelineItem("nomination", "Nominierung", season.NominationStartsAt, season.NominationEndsAt, "done"),
            new TimelineItem("voting", "Voting", season.VotingStartsAt, season.VotingEndsAt, "active"),
            new TimelineItem("review", "Auswertung", season.ReviewStartsAt, season.ReviewEndsAt, "upcoming"),
            new TimelineItem("show", "Award Show", season.ShowDate, season.ShowDate, "upcoming"),
        },
        season.Categories
            .Take(6)
            .Select(category => new FeaturedCategoryDto(
                category.Id,
                category.GroupName,
                category.Name,
                category.Description,
                category.MaxNomineesPerUser))
            .ToArray(),
        season.Results
            .OrderByDescending(result => result.SeasonId)
            .Take(4)
            .Select(result => new WinnerPreviewDto(
                season.Year,
                result.CategoryName,
                result.Candidate.DisplayName,
                result.Candidate.ChannelSlug))
            .ToArray(),
        new[]
        {
            new FaqItemDto("Wer kann nominieren und voten?", "Jede Person mit Twitch Login. Das Konto wird beim ersten Login implizit erstellt."),
            new FaqItemDto("Wie werden Gewinner bestimmt?", "Aktuell rein community-basiert. Eine Mischlogik mit Jury oder Panel kann spaeter eingefuehrt werden."),
            new FaqItemDto("Wer verwaltet Kategorien und Unterkategorien?", "Das Team pflegt diese pro Jahr im Admin-Bereich."),
        });

    return Results.Ok(response);
})
.WithName("GetOverview")
.WithOpenApi();

app.MapGet("/api/public/seasons/{year:int}/categories", async (int year, AwardsDbContext db) =>
{
    var season = await db.Seasons
        .AsNoTracking()
        .Include(item => item.Categories.OrderBy(category => category.SortOrder))
        .ThenInclude(category => category.Candidates.OrderBy(candidate => candidate.DisplayName))
        .FirstOrDefaultAsync(item => item.Year == year);

    if (season is null)
    {
        return Results.NotFound();
    }

    return Results.Ok(new SeasonCategoriesResponse(
        season.Id,
        season.Year,
        season.Categories.Select(category => new PublicCategoryDetailDto(
            category.Id,
            category.Name,
            category.GroupName,
            category.Description,
            category.MaxNomineesPerUser,
            category.Candidates.Select(candidate => new CandidateSummaryDto(
                candidate.Id,
                candidate.DisplayName,
                candidate.ChannelSlug,
                candidate.Platform))
                .ToArray()))
            .ToArray()));
})
.WithName("GetSeasonCategories")
.WithOpenApi();

app.MapGet("/api/public/seasons/{year:int}/winners", async (int year, AwardsDbContext db) =>
{
    var items = await db.Results
        .AsNoTracking()
        .Include(result => result.Candidate)
        .Where(result => result.Season.Year == year)
        .OrderBy(result => result.CategoryName)
        .Select(result => new WinnerArchiveItemDto(
            result.CategoryName,
            result.Candidate.DisplayName,
            result.Candidate.ChannelSlug))
        .ToArrayAsync();

    return Results.Ok(new WinnerArchiveResponse(year, items));
})
.WithName("GetWinnerArchive")
.WithOpenApi();

app.MapPost("/api/public/nominations", async (HttpContext context, CreateNominationRequest request, AwardsDbContext db) =>
{
    if (request.Nominees.Length is 0 or > 3)
    {
        return Results.BadRequest(new { message = "A nomination request must include between 1 and 3 nominees." });
    }

    var distinctNominees = request.Nominees
        .Select(item => item.Trim())
        .Where(item => !string.IsNullOrWhiteSpace(item))
        .Distinct(StringComparer.OrdinalIgnoreCase)
        .ToArray();

    if (distinctNominees.Length != request.Nominees.Length)
    {
        return Results.BadRequest(new { message = "Duplicate nominees are not allowed inside one category." });
    }

    var category = await db.Categories
        .Include(item => item.Season)
        .FirstOrDefaultAsync(item => item.Id == request.CategoryId && item.Season.Year == request.Year);

    if (category is null)
    {
        return Results.BadRequest(new { message = "The selected category does not exist for this season." });
    }

    var session = await ResolveSessionAsync(context, db);
    var submitterId = session?.TwitchUserId ?? request.TwitchUserId;
    if (string.IsNullOrWhiteSpace(submitterId))
    {
        return Results.BadRequest(new { message = "A logged in user is required to submit nominations." });
    }

    var createdFromIp = ReadClientIp(context);
    var userAgent = ReadUserAgent(context);
    var existingNominationCount = await db.Nominations.CountAsync(item =>
        item.SeasonId == category.SeasonId
        && item.CategoryId == category.Id
        && item.SubmittedByTwitchId == submitterId);

    var previousNominations = await db.Nominations
        .Where(item =>
            item.SeasonId == category.SeasonId
            && item.CategoryId == category.Id
            && item.SubmittedByTwitchId == submitterId)
        .ToArrayAsync();

    if (previousNominations.Length > 0)
    {
        db.Nominations.RemoveRange(previousNominations);
    }

    var records = distinctNominees.Select(name => new Nomination
    {
        SeasonId = category.SeasonId,
        CategoryId = category.Id,
        SubmittedByTwitchId = submitterId,
        CandidateText = name,
        CreatedAt = DateTimeOffset.UtcNow,
    });

    await db.Nominations.AddRangeAsync(records);

    var recentNominationVolume = await db.Nominations.CountAsync(item =>
        item.SubmittedByTwitchId == submitterId
        && item.CreatedAt >= DateTimeOffset.UtcNow.AddMinutes(-10));

    if (existingNominationCount > 0)
    {
        await AddRiskFlagIfMissingAsync(
            db,
            category.SeasonId,
            submitterId,
            "nomination",
            "resubmitted_nomination",
            "low",
            "Ein User hat seine Nominierung in derselben Kategorie erneut eingereicht.",
            createdFromIp,
            userAgent,
            new { categoryId = category.Id, existingNominationCount });
    }

    if (recentNominationVolume >= 10)
    {
        await AddRiskFlagIfMissingAsync(
            db,
            category.SeasonId,
            submitterId,
            "nomination",
            "rapid_nomination_burst",
            "high",
            "Ungewoehnlich viele Nominierungsaktionen in kurzer Zeit erkannt.",
            createdFromIp,
            userAgent,
            new { recentNominationVolume });
    }

    await db.SaveChangesAsync();

    return Results.Ok(new { saved = distinctNominees.Length, category = category.Name, replacedPrevious = previousNominations.Length > 0 });
})
.WithName("CreateNomination")
.WithOpenApi();

app.MapPost("/api/public/votes", async (HttpContext context, CreateVoteRequest request, AwardsDbContext db) =>
{
    if (request.Entries.Length == 0)
    {
        return Results.BadRequest(new { message = "At least one vote entry is required." });
    }

    var distinctCategoryCount = request.Entries
        .Select(item => item.CategoryId)
        .Distinct()
        .Count();

    if (distinctCategoryCount != request.Entries.Length)
    {
        return Results.BadRequest(new { message = "Only one vote entry per category is allowed." });
    }

    var session = await ResolveSessionAsync(context, db);
    var submitterId = session?.TwitchUserId ?? request.TwitchUserId;
    if (string.IsNullOrWhiteSpace(submitterId))
    {
        return Results.BadRequest(new { message = "A logged in user is required to submit votes." });
    }

    var createdFromIp = ReadClientIp(context);
    var userAgent = ReadUserAgent(context);
    var candidateIds = request.Entries.Select(item => item.CandidateId).Distinct().ToArray();
    var validCandidates = await db.Candidates
        .AsNoTracking()
        .Where(item => item.SeasonId == request.SeasonId && candidateIds.Contains(item.Id))
        .Select(item => new { item.Id, item.CategoryId })
        .ToArrayAsync();

    if (validCandidates.Length != candidateIds.Length)
    {
        return Results.BadRequest(new { message = "One or more selected candidates do not belong to this season." });
    }

    var candidateCategoryMap = validCandidates.ToDictionary(item => item.Id, item => item.CategoryId);
    if (request.Entries.Any(item => candidateCategoryMap[item.CandidateId] != item.CategoryId))
    {
        return Results.BadRequest(new { message = "A selected candidate does not match the submitted category." });
    }

    var ballot = await db.VoteBallots
        .Include(item => item.Entries)
        .FirstOrDefaultAsync(item => item.SeasonId == request.SeasonId && item.SubmittedByTwitchId == submitterId);

    var isResubmission = ballot is not null;
    if (ballot is null)
    {
        ballot = new VoteBallot
        {
            SeasonId = request.SeasonId,
            SubmittedByTwitchId = submitterId,
        };

        await db.VoteBallots.AddAsync(ballot);
    }
    else
    {
        db.VoteEntries.RemoveRange(ballot.Entries);
        ballot.Entries.Clear();
    }

    ballot.SubmittedAt = DateTimeOffset.UtcNow;
    ballot.Status = "submitted";
    ballot.Entries = request.Entries.Select(entry => new VoteEntry
    {
        CategoryId = entry.CategoryId,
        CandidateId = entry.CandidateId,
    }).ToList();

    var recentVoteSubmissions = await db.VoteBallots.CountAsync(item =>
        item.SubmittedByTwitchId == submitterId
        && item.SubmittedAt >= DateTimeOffset.UtcNow.AddMinutes(-10));

    if (isResubmission)
    {
        await AddRiskFlagIfMissingAsync(
            db,
            request.SeasonId,
            submitterId,
            "vote",
            "resubmitted_ballot",
            "low",
            "Ein User hat sein Ballot erneut gespeichert oder aktualisiert.",
            createdFromIp,
            userAgent,
            new { entryCount = request.Entries.Length });
    }

    if (recentVoteSubmissions >= 3)
    {
        await AddRiskFlagIfMissingAsync(
            db,
            request.SeasonId,
            submitterId,
            "vote",
            "rapid_vote_updates",
            "high",
            "Mehrere Voting-Aenderungen wurden in kurzer Zeit erkannt.",
            createdFromIp,
            userAgent,
            new { recentVoteSubmissions });
    }

    await db.SaveChangesAsync();

    return Results.Ok(new { ballotId = ballot.Id, entries = ballot.Entries.Count, updated = isResubmission });
})
.WithName("CreateVote")
.WithOpenApi();

app.MapGet("/api/admin/dashboard", async (HttpContext context, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var currentSeason = await db.Seasons.AsNoTracking().FirstOrDefaultAsync(item => item.IsCurrent);
    if (currentSeason is null)
    {
        return Results.NotFound();
    }

    var nominationCount = await db.Nominations.CountAsync(item => item.SeasonId == currentSeason.Id);
    var voteCount = await db.VoteEntries.CountAsync(item => item.Ballot.SeasonId == currentSeason.Id);
    var categoryCount = await db.Categories.CountAsync(item => item.SeasonId == currentSeason.Id);
    var reviewCount = await db.Nominations.CountAsync(item => item.SeasonId == currentSeason.Id && item.CandidateText != null);

    var topCategoryNames = await db.VoteEntries
        .AsNoTracking()
        .Where(item => item.Ballot.SeasonId == currentSeason.Id)
        .Select(item => item.Category.Name)
        .ToListAsync();

    var topCategories = topCategoryNames
        .GroupBy(name => name)
        .Select(group => new AdminTopCategoryDto(group.Key, group.Count()))
        .OrderByDescending(item => item.Votes)
        .Take(5)
        .ToArray();

    var riskFlags = await db.RiskFlags
        .AsNoTracking()
        .Where(item => item.Status == "open")
        .OrderByDescending(item => item.CreatedAt)
        .Take(8)
        .Select(item => new AdminRiskFlagDto(
            item.Id,
            item.Source,
            item.Type,
            item.Severity,
            item.Status,
            item.Summary,
            item.TwitchUserId,
            item.CreatedFromIp,
            item.CreatedAt,
            item.MetadataJson))
        .ToArrayAsync();

    var auditEntries = await db.AdminAuditEntries
        .AsNoTracking()
        .OrderByDescending(item => item.CreatedAt)
        .Take(8)
        .Select(item => new AdminAuditEntryDto(
            item.Id,
            item.AdminTwitchUserId,
            item.ActionType,
            item.EntityType,
            item.EntityId,
            item.Summary,
            item.CreatedAt))
        .ToArrayAsync();

    var activityItems = auditEntries
        .Take(3)
        .Select(item => new AdminActivityDto(item.Summary, $"{Math.Max(1, (int)Math.Round((DateTimeOffset.UtcNow - item.CreatedAt).TotalMinutes))} Min."))
        .ToArray();

    var response = new AdminDashboardResponse(
        new[]
        {
            new AdminMetricDto("Nominierungen", nominationCount, "+12.4% vs. gestern"),
            new AdminMetricDto("Votes", voteCount, "+8.7% vs. gestern"),
            new AdminMetricDto("Kategorien", categoryCount, "aktiv im aktuellen Jahr"),
            new AdminMetricDto("Reviews offen", reviewCount, "Freitext und Dubletten"),
        },
        activityItems,
        topCategories,
        riskFlags,
        auditEntries);

    return Results.Ok(response);
})
.WithName("GetAdminDashboard")
.WithOpenApi();

app.MapGet("/api/admin/seasons", async (HttpContext context, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var seasons = await db.Seasons
        .AsNoTracking()
        .OrderByDescending(item => item.Year)
        .Select(item => new AdminSeasonListItemDto(
            item.Id,
            item.Year,
            item.Name,
            item.CurrentPhase,
            item.IsCurrent,
            item.Categories.Count))
        .ToArrayAsync();

    return Results.Ok(seasons);
})
.WithName("GetAdminSeasons")
.WithOpenApi();

app.MapGet("/api/admin/seasons/{seasonId:int}", async (HttpContext context, int seasonId, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var season = await db.Seasons
        .AsNoTracking()
        .FirstOrDefaultAsync(item => item.Id == seasonId);

    if (season is null)
    {
        return Results.NotFound();
    }

    var candidates = await db.Candidates
        .AsNoTracking()
        .Where(item => item.SeasonId == seasonId)
        .OrderBy(item => item.DisplayName)
        .Select(item => new AdminCandidateItemDto(
            item.Id,
            item.CategoryId,
            item.DisplayName,
            item.ChannelSlug,
            item.Platform))
        .ToArrayAsync();

    var candidateCounts = candidates
        .GroupBy(item => item.CategoryId)
        .ToDictionary(group => group.Key, group => group.Count());

    var categoryRows = await db.Categories
        .AsNoTracking()
        .Where(item => item.SeasonId == seasonId)
        .OrderBy(item => item.SortOrder)
        .ThenBy(item => item.Name)
        .Select(category => new
        {
            category.Id,
            category.GroupName,
            category.Name,
            category.Slug,
            category.Description,
            category.SortOrder,
            category.MaxNomineesPerUser,
        })
        .ToArrayAsync();

    var categories = categoryRows
        .Select(category => new AdminCategoryItemDto(
            category.Id,
            category.GroupName,
            category.Name,
            category.Slug,
            category.Description,
            category.SortOrder,
            category.MaxNomineesPerUser,
            candidateCounts.TryGetValue(category.Id, out var count) ? count : 0))
        .ToArray();

    var pendingNominations = await db.Nominations
        .AsNoTracking()
        .Where(item => item.SeasonId == seasonId && item.CandidateText != null)
        .OrderByDescending(item => item.CreatedAt)
        .Take(20)
        .Select(item => new AdminNominationReviewItemDto(
            item.Id,
            item.CategoryId,
            item.Category.Name,
            item.SubmittedByTwitchId,
            item.CandidateText!,
            item.CreatedAt))
        .ToArrayAsync();

    return Results.Ok(new AdminSeasonDetailResponse(
        season.Id,
        season.Year,
        season.Name,
        season.CurrentPhase,
        season.IsCurrent,
        categories,
        candidates,
        pendingNominations));
})
.WithName("GetAdminSeasonDetail")
.WithOpenApi();

app.MapPut("/api/admin/seasons/{seasonId:int}", async (HttpContext context, int seasonId, UpdateSeasonRequest request, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId);
    if (season is null)
    {
        return Results.NotFound();
    }

    season.CurrentPhase = request.CurrentPhase.Trim();

    if (request.IsCurrent && !season.IsCurrent)
    {
        var activeSeasons = await db.Seasons.Where(item => item.IsCurrent && item.Id != seasonId).ToListAsync();
        foreach (var activeSeason in activeSeasons)
        {
            activeSeason.IsCurrent = false;
        }
    }

    season.IsCurrent = request.IsCurrent;
    AddAuditEntry(
        db,
        session.TwitchUserId,
        "season.update",
        "season",
        season.Id.ToString(),
        $"Season {season.Year} wurde aktualisiert.",
        new { request.CurrentPhase, request.IsCurrent });
    await db.SaveChangesAsync();

    return Results.Ok(new { saved = true, seasonId = season.Id });
})
.WithName("UpdateAdminSeason")
.WithOpenApi();

app.MapPost("/api/admin/seasons/{seasonId:int}/categories", async (HttpContext context, int seasonId, UpsertCategoryRequest request, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId);
    if (season is null)
    {
        return Results.NotFound();
    }

    var category = new Category
    {
        SeasonId = seasonId,
        GroupName = request.GroupName.Trim(),
        Name = request.Name.Trim(),
        Slug = request.Slug.Trim(),
        Description = request.Description.Trim(),
        SortOrder = request.SortOrder,
        MaxNomineesPerUser = request.MaxNomineesPerUser,
    };

    db.Categories.Add(category);
    AddAuditEntry(
        db,
        session.TwitchUserId,
        "category.create",
        "category",
        request.Slug.Trim(),
        $"Kategorie {request.Name.Trim()} wurde angelegt.",
        new { seasonId, request.GroupName, request.SortOrder });
    await db.SaveChangesAsync();

    return Results.Ok(new { saved = true, categoryId = category.Id });
})
.WithName("CreateAdminCategory")
.WithOpenApi();

app.MapPut("/api/admin/categories/{categoryId:int}", async (HttpContext context, int categoryId, UpsertCategoryRequest request, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var category = await db.Categories.FirstOrDefaultAsync(item => item.Id == categoryId);
    if (category is null)
    {
        return Results.NotFound();
    }

    category.GroupName = request.GroupName.Trim();
    category.Name = request.Name.Trim();
    category.Slug = request.Slug.Trim();
    category.Description = request.Description.Trim();
    category.SortOrder = request.SortOrder;
    category.MaxNomineesPerUser = request.MaxNomineesPerUser;

    AddAuditEntry(
        db,
        session.TwitchUserId,
        "category.update",
        "category",
        category.Id.ToString(),
        $"Kategorie {request.Name.Trim()} wurde aktualisiert.",
        new { request.GroupName, request.SortOrder, request.MaxNomineesPerUser });
    await db.SaveChangesAsync();

    return Results.Ok(new { saved = true, categoryId = category.Id });
})
.WithName("UpdateAdminCategory")
.WithOpenApi();

app.MapPost("/api/admin/seasons/{seasonId:int}/candidates", async (HttpContext context, int seasonId, UpsertCandidateRequest request, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var category = await db.Categories.FirstOrDefaultAsync(item => item.Id == request.CategoryId && item.SeasonId == seasonId);
    if (category is null)
    {
        return Results.BadRequest(new { message = "The selected category does not exist in this season." });
    }

    var candidate = new Candidate
    {
        SeasonId = seasonId,
        CategoryId = request.CategoryId,
        DisplayName = request.DisplayName.Trim(),
        ChannelSlug = request.ChannelSlug.Trim(),
        Platform = request.Platform.Trim(),
    };

    db.Candidates.Add(candidate);
    AddAuditEntry(
        db,
        session.TwitchUserId,
        "candidate.create",
        "candidate",
        request.DisplayName.Trim(),
        $"Kandidat {request.DisplayName.Trim()} wurde angelegt.",
        new { seasonId, request.CategoryId, request.Platform });
    await db.SaveChangesAsync();

    return Results.Ok(new { saved = true, candidateId = candidate.Id });
})
.WithName("CreateAdminCandidate")
.WithOpenApi();

app.MapPut("/api/admin/candidates/{candidateId:int}", async (HttpContext context, int candidateId, UpsertCandidateRequest request, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var candidate = await db.Candidates.FirstOrDefaultAsync(item => item.Id == candidateId);
    if (candidate is null)
    {
        return Results.NotFound();
    }

    candidate.CategoryId = request.CategoryId;
    candidate.DisplayName = request.DisplayName.Trim();
    candidate.ChannelSlug = request.ChannelSlug.Trim();
    candidate.Platform = request.Platform.Trim();

    AddAuditEntry(
        db,
        session.TwitchUserId,
        "candidate.update",
        "candidate",
        candidate.Id.ToString(),
        $"Kandidat {request.DisplayName.Trim()} wurde aktualisiert.",
        new { request.CategoryId, request.Platform });
    await db.SaveChangesAsync();

    return Results.Ok(new { saved = true, candidateId = candidate.Id });
})
.WithName("UpdateAdminCandidate")
.WithOpenApi();

app.MapPost("/api/admin/nominations/{nominationId:int}/approve", async (HttpContext context, int nominationId, ApproveNominationRequest request, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var nomination = await db.Nominations
        .Include(item => item.Category)
        .FirstOrDefaultAsync(item => item.Id == nominationId);

    if (nomination is null)
    {
        return Results.NotFound();
    }

    var rawDisplayName = string.IsNullOrWhiteSpace(request.DisplayName)
        ? nomination.CandidateText
        : request.DisplayName.Trim();

    if (string.IsNullOrWhiteSpace(rawDisplayName))
    {
        return Results.BadRequest(new { message = "A display name is required to approve the nomination." });
    }

    var channelSlug = request.ChannelSlug?.Trim() ?? string.Empty;
    var platform = string.IsNullOrWhiteSpace(request.Platform) ? "Twitch" : request.Platform.Trim();

    var existingCandidate = await db.Candidates.FirstOrDefaultAsync(item =>
        item.SeasonId == nomination.SeasonId
        && item.CategoryId == nomination.CategoryId
        && item.DisplayName.ToLower() == rawDisplayName.ToLower());

    var candidate = existingCandidate;
    if (candidate is null)
    {
        candidate = new Candidate
        {
            SeasonId = nomination.SeasonId,
            CategoryId = nomination.CategoryId,
            DisplayName = rawDisplayName,
            ChannelSlug = channelSlug,
            Platform = platform,
        };

        db.Candidates.Add(candidate);
        await db.SaveChangesAsync();
    }
    else
    {
        if (!string.IsNullOrWhiteSpace(channelSlug))
        {
            candidate.ChannelSlug = channelSlug;
        }

        if (!string.IsNullOrWhiteSpace(platform))
        {
            candidate.Platform = platform;
        }
    }

    nomination.CandidateId = candidate.Id;
    nomination.CandidateText = null;
    AddAuditEntry(
        db,
        session.TwitchUserId,
        "nomination.approve",
        "nomination",
        nomination.Id.ToString(),
        $"Nominierung {nomination.Id} wurde als Kandidat uebernommen.",
        new { candidateId = candidate.Id, created = existingCandidate is null });
    await db.SaveChangesAsync();

    return Results.Ok(new { saved = true, nominationId = nomination.Id, candidateId = candidate.Id, created = existingCandidate is null });
})
.WithName("ApproveAdminNomination")
.WithOpenApi();

app.MapPost("/api/admin/nominations/{nominationId:int}/reject", async (HttpContext context, int nominationId, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var nomination = await db.Nominations.FirstOrDefaultAsync(item => item.Id == nominationId);
    if (nomination is null)
    {
        return Results.NotFound();
    }

    nomination.CandidateText = null;
    nomination.CandidateId = null;
    AddAuditEntry(
        db,
        session.TwitchUserId,
        "nomination.reject",
        "nomination",
        nomination.Id.ToString(),
        $"Nominierung {nomination.Id} wurde verworfen.");
    await db.SaveChangesAsync();

    return Results.Ok(new { saved = true, nominationId = nomination.Id, rejected = true });
})
.WithName("RejectAdminNomination")
.WithOpenApi();

app.MapPost("/api/admin/risk-flags/{riskFlagId:int}/resolve", async (HttpContext context, int riskFlagId, ResolveRiskFlagRequest request, AwardsDbContext db) =>
{
    var session = await ResolveSessionAsync(context, db);
    if (session?.Role != "admin")
    {
        return Results.Unauthorized();
    }

    var riskFlag = await db.RiskFlags.FirstOrDefaultAsync(item => item.Id == riskFlagId);
    if (riskFlag is null)
    {
        return Results.NotFound();
    }

    riskFlag.Status = string.IsNullOrWhiteSpace(request.Status) ? "resolved" : request.Status.Trim().ToLowerInvariant();
    riskFlag.ReviewedAt = DateTimeOffset.UtcNow;
    riskFlag.ReviewedByTwitchId = session.TwitchUserId;

    AddAuditEntry(
        db,
        session.TwitchUserId,
        "risk.resolve",
        "risk-flag",
        riskFlag.Id.ToString(),
        $"Risk Flag {riskFlag.Id} wurde als {riskFlag.Status} markiert.",
        new { riskFlag.Type, riskFlag.Source });

    await db.SaveChangesAsync();

    return Results.Ok(new { saved = true, riskFlagId = riskFlag.Id, status = riskFlag.Status });
})
.WithName("ResolveRiskFlag")
.WithOpenApi();

app.Run();
