using BCrypt.Net;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.IdentityModel.Tokens;
using MySqlConnector;
using System.Collections.Generic;
using System.Data;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("Default");
var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "CHANGE_ME_TO_A_LONG_RANDOM_SECRET";
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "Ticket-System";
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "Ticket-System";

builder.Services.AddTransient(_ => new MySqlConnection(connectionString));

var app = builder.Build();

app.Use(async (context, next) =>
{
    var path = context.Request.Path.Value ?? "";

    if (path.Equals("/signin", StringComparison.OrdinalIgnoreCase)) //|| path.Equals("/user/create", StringComparison.OrdinalIgnoreCase))
    {
        await next();
        return;
    }

    if (!context.Request.Headers.TryGetValue("Authorization", out var authHeaderValues))
    {
        context.Response.StatusCode = StatusCodes.Status403Forbidden;
        await context.Response.WriteAsJsonAsync(new { error = "Please add the JWT token to the header" });
        return;
    }

    var authHeader = authHeaderValues.ToString();
    var parts = authHeader.Split(' ', StringSplitOptions.RemoveEmptyEntries);

    if (parts.Length != 2 || !parts[0].Equals("Bearer", StringComparison.OrdinalIgnoreCase))
    {
        context.Response.StatusCode = StatusCodes.Status409Conflict;
        await context.Response.WriteAsJsonAsync(new { error = "Invalid token specified" });
        return;
    }

    var token = parts[1];

    var principal = ValidateJwt(token, jwtSecret, jwtIssuer, jwtAudience);
    if (principal == null)
    {
        context.Response.StatusCode = StatusCodes.Status403Forbidden;
        await context.Response.WriteAsJsonAsync(new { error = "Invalid token specified" });
        return;
    }

    context.Items["user"] = principal.Identity?.Name ?? principal.FindFirstValue(ClaimTypes.Name) ?? "";
    await next();
});

app.MapPost("/signin", async (SignInRequest req, MySqlConnection conn) =>
{
    const string sql = "SELECT userID, username, password FROM User WHERE username = @username LIMIT 1;";

    await conn.OpenAsync();

    await using var cmd = new MySqlCommand(sql, conn);
    cmd.Parameters.AddWithValue("@username", req.name);

    await using var reader = await cmd.ExecuteReaderAsync();

    if (!await reader.ReadAsync())
        return Results.Unauthorized();

    var userId = reader.GetInt32("userID").ToString();
    var username = reader.GetString("username");
    var pwHash = reader.GetString("password");

    var ok = BCrypt.Net.BCrypt.Verify(req.password, pwHash);
    if (!ok)
        return Results.Unauthorized();

    var token = CreateJwt(userId, username, jwtSecret, jwtIssuer, jwtAudience, minutesValid: 120);
    return Results.Ok(new ResponseToken(token));
});

app.MapGet("/token/validate", (HttpContext ctx) =>
{
    return Results.Ok(new TokenIsValid(true));
});

app.MapGet("/me", (HttpContext ctx) =>
{
    var user = ctx.Items["user"]?.ToString() ?? "";
    return Results.Ok(new { user });
});


app.MapPost("/user/create", async (RegisterRequest req, MySqlConnection conn) =>
{
    await conn.OpenAsync();

    var pwHash = BCrypt.Net.BCrypt.HashPassword(req.password);
    var userId = Guid.NewGuid().ToString("N");

    const string insertSql = """
        INSERT INTO User (userID, username, password)
        VALUES (@userID, @username, @password);
    """;

    await using var cmd = new MySqlCommand(insertSql, conn);
    cmd.Parameters.AddWithValue("@userID", userId);
    cmd.Parameters.AddWithValue("@username", req.name);
    cmd.Parameters.AddWithValue("@password", pwHash);

    var rows = await cmd.ExecuteNonQueryAsync();
    if (rows != 1)
        return Results.Problem("User konnte nicht angelegt werden.");

    var token = CreateJwt(userId, req.name, jwtSecret, jwtIssuer, jwtAudience, minutesValid: 120);

    return Results.Created($"/user/{userId}", new ResponseToken(token));
});

app.MapPost("/ticket/create", async (HttpContext ctx, CreateTicketRequest req, MySqlConnection conn) =>
{
    var userId = ctx.Items["user"]?.ToString();
    if (string.IsNullOrEmpty(userId))
        return Results.Unauthorized();

    await conn.OpenAsync();

    const string insertSql = """
        INSERT INTO ticket (userID, ticketID, ticketname, status, priority, OpenAt, property)
        VALUES (@userID,(select max(ticketID) from ticket) + 1, @ticketname, @beschreibung, @status, @priority, @openAt, @property);
    """;

    await using var cmd = new MySqlCommand(insertSql, conn);
    cmd.Parameters.AddWithValue("@userID", userId);
    cmd.Parameters.AddWithValue("@status", req.status);
    cmd.Parameters.AddWithValue("@ticketname", req.ticketname);
    cmd.Parameters.AddWithValue("@beschreibung", req.beschreibung);
    cmd.Parameters.AddWithValue("@priority", req.priority);
    cmd.Parameters.AddWithValue("@openAt", DateTime.UtcNow);
    cmd.Parameters.AddWithValue("@property", req.property);

    var rows = await cmd.ExecuteNonQueryAsync();
    if (rows != 1)
        return Results.Problem("Ticket konnte nicht angelegt werden");

    var ticketId = cmd.LastInsertedId;

    return Results.Created($"/ticket/{ticketId}", new
    {
        ticketID = ticketId,
        userID = userId
    });
});

app.MapGet("/ticket/show/all", async (HttpContext ctx, MySqlConnection conn) =>
{
    var userId = ctx.Items["user"]?.ToString();
    if (string.IsNullOrEmpty(userId))
        return Results.Unauthorized();

    await conn.OpenAsync();

    const string sql = """
        SELECT 
            t.ticketID,
            t.ticketname,
            u.username,
            t.status,
            t.priority,
            t.category
        FROM ticket t
        JOIN User u ON t.userID = u.userID
        WHERE t.userID = @userID
        ORDER BY t.priority;
    """;

    await using var cmd = new MySqlCommand(sql, conn);
    cmd.Parameters.AddWithValue("@userID", userId);

    await using var reader = await cmd.ExecuteReaderAsync();

    var tickets = new List<object>();

    while (await reader.ReadAsync())
    {
        tickets.Add(new
        {
            ticketID = reader.GetInt64("ticketID"),
            ticketname = reader.GetString("ticketname"),
            username = reader.GetString("username"),
            status = reader.GetInt32("status"),
            priority = reader.GetInt32("priority"),
            category = reader.GetString("category")
        });
    }

    return Results.Ok(tickets);
});


app.MapGet("/ticket/show/{ticketId:int}", async (
    HttpContext ctx,
    long ticketId,
    MySqlConnection conn) =>
{
    var userId = ctx.Items["user"]?.ToString();
    if (string.IsNullOrEmpty(userId))
        return Results.Unauthorized();

    await conn.OpenAsync();

    const string ticketSql = """
        SELECT
            t.TicketID,
            t.Ticketname,
            t.UserID,
            u.Username AS OwnerUsername,
            t.Status,
            t.Priority,
            t.OpenAt,
            t.ClosedAt,
            t.category
        FROM Ticket t
        JOIN User u ON u.UserID = t.UserID
        WHERE t.TicketID = @ticketId
          AND t.UserID = @userId;
    """;

    object? ticket = null;

    await using (var cmd = new MySqlCommand(ticketSql, conn))
    {
        cmd.Parameters.AddWithValue("@ticketId", ticketId);
        cmd.Parameters.AddWithValue("@userId", userId);

        await using var reader = await cmd.ExecuteReaderAsync();
        if (!await reader.ReadAsync())
            return Results.NotFound("Ticket nicht gefunden");

        ticket = new
        {
            ticketID = reader.GetInt64("TicketID"),
            ticketname = reader.GetString("Ticketname"),
            userID = reader.GetInt64("UserID"),
            ownerUsername = reader.GetString("OwnerUsername"),
            status = reader.GetInt32("Status"),
            priority = reader.GetInt32("Priority"),
            openAt = reader.GetString("OpenAt"),
            closedAt = reader.IsDBNull("ClosedAt") ? null : reader.GetString("ClosedAt"),
            category = reader.GetString("category")
        };
    }

    const string messagesSql = """
        SELECT
            m.MessageID,
            m.Sequence,
            m.SendAt,
            m.Content,
            m.Sender,
            su.Username AS SenderUsername,
            m.Reciver,
            ru.Username AS ReciverUsername
        FROM Messages m
        LEFT JOIN User su ON su.UserID = m.Sender
        LEFT JOIN User ru ON ru.UserID = m.Reciver
        WHERE m.TicketID = @ticketId
        ORDER BY m.Sequence ASC;
    """;

    var messages = new List<object>();

    await using (var cmd = new MySqlCommand(messagesSql, conn))
    {
        cmd.Parameters.AddWithValue("@ticketId", ticketId);

        await using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            messages.Add(new
            {
                messageID = reader.GetInt64("MessageID"),
                sequence = reader.GetInt32("Sequence"),
                sendAt = reader.GetString("SendAt"),
                content = reader.GetString("Content"),
                sender = reader.GetInt64("Sender"),
                senderUsername = reader.IsDBNull("SenderUsername") ? null : reader.GetString("SenderUsername"),
                reciver = reader.GetInt64("Reciver"),
                reciverUsername = reader.IsDBNull("ReciverUsername") ? null : reader.GetString("ReciverUsername")
            });
        }
    }

    return Results.Ok(new
    {
        ticket,
        messages
    });
});

static string CreateJwt(string userId, string username, string secret, string issuer, string audience, int minutesValid)
{
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.NameIdentifier, userId),
        new Claim(ClaimTypes.Name, username),
    };

    var token = new JwtSecurityToken(
        issuer: issuer,
        audience: audience,
        claims: claims,
        notBefore: DateTime.UtcNow,
        expires: DateTime.UtcNow.AddMinutes(minutesValid),
        signingCredentials: creds
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

static ClaimsPrincipal? ValidateJwt(string token, string secret, string issuer, string audience)
{
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
    var handler = new JwtSecurityTokenHandler();

    try
    {
        var principal = handler.ValidateToken(token, new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = issuer,
            ValidateAudience = true,
            ValidAudience = audience,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = key,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromSeconds(30)
        }, out _);

        return principal;
    }
    catch
    {
        return null;
    }
    ;
}

app.Run();

public record RegisterRequest(string name, string password);
public record SignInRequest(string name, string password);
public record ResponseToken(string token);
public record TokenIsValid(bool is_valid);
public record CreateTicketRequest(
    int status,
    int priority,
    string property,
    string ticketname,
    string beschreibung
);