using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; using System.Text.Json; using SilverLabs.Website.Models; namespace SilverLabs.Website.Services; public class DeveloperApplicationService { private readonly HttpClient _httpClient; private readonly ILogger _logger; public DeveloperApplicationService(HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; } /// /// Checks username availability. Returns: true = available, false = taken, null = error/unknown. /// public async Task CheckUsernameAsync(string username) { try { var response = await _httpClient.GetAsync($"/api/auth/check-username/{Uri.EscapeDataString(username)}"); if (!response.IsSuccessStatusCode) { _logger.LogWarning("Username check returned {StatusCode} for {Username}", response.StatusCode, username); return null; } var result = await response.Content.ReadFromJsonAsync(); if (result.TryGetProperty("available", out var available)) return available.GetBoolean(); return null; } catch (Exception ex) { _logger.LogError(ex, "Error checking username availability for {Username}", username); return null; } } public async Task<(bool Success, string Message, string? Token)> SubmitApplicationAsync(DeveloperApplication application) { try { // Use silverlabs.uk address when no personal email provided var effectiveEmail = string.IsNullOrWhiteSpace(application.Email) ? $"{application.DesiredUsername}@silverlabs.uk" : application.Email.Trim(); // 1. Register user on SilverDESK var registerPayload = new { username = application.DesiredUsername, email = effectiveEmail, password = application.Password, fullName = application.FullName }; var registerResponse = await _httpClient.PostAsJsonAsync("/api/auth/register", registerPayload); if (!registerResponse.IsSuccessStatusCode) { var errorBody = await registerResponse.Content.ReadAsStringAsync(); _logger.LogError("SilverDESK registration failed: {StatusCode} - {Body}", registerResponse.StatusCode, errorBody); var friendlyMessage = ParseRegistrationError(errorBody); return (false, friendlyMessage, null); } var authResult = await registerResponse.Content.ReadFromJsonAsync(); var token = authResult.GetProperty("token").GetString(); // 2. Create ticket using the user's own JWT var ticketBody = FormatTicketBody(application); var ticketPayload = new { Subject = $"[Developer Program] {application.Role} Application - {application.FullName}", Description = ticketBody, Priority = "Medium", Category = "Developer Program" }; // Use a fresh HttpClient without the X-API-Key default header so that // SilverDESK's MultiAuth policy routes to Bearer/JWT auth (the new user's token) // instead of ApiKey auth (which resolves to the MCP system user). using var userClient = new HttpClient { BaseAddress = _httpClient.BaseAddress }; var ticketRequest = new HttpRequestMessage(HttpMethod.Post, "/api/tickets"); ticketRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); ticketRequest.Content = JsonContent.Create(ticketPayload); var ticketResponse = await userClient.SendAsync(ticketRequest); if (!ticketResponse.IsSuccessStatusCode) { var errorBody = await ticketResponse.Content.ReadAsStringAsync(); _logger.LogError("Failed to create ticket: {StatusCode} - {Body}", ticketResponse.StatusCode, errorBody); // User was created but ticket failed — still return success with a note return (true, "Your account has been created, but we had trouble submitting your application ticket. Please log in to SilverDESK and create a support ticket.", token); } // 3. Create DeveloperApplication record linking user + ticket try { var userId = authResult.GetProperty("user").GetProperty("id").GetString(); var ticketResult = await ticketResponse.Content.ReadFromJsonAsync(); var ticketId = ticketResult.GetProperty("id").GetString(); var applicationPayload = new { userId, ticketId, fullName = application.FullName, email = effectiveEmail, desiredUsername = application.DesiredUsername, timezone = application.Timezone, appliedRole = application.Role.ToString(), platforms = application.Platforms, skills = SerializeAssessment(application), motivation = GenerateMotivationSummary(application), status = 0, // Pending silverDeskProvisioned = true }; var appResponse = await _httpClient.PostAsJsonAsync("/api/developer-program/applications", applicationPayload); if (appResponse.IsSuccessStatusCode) { _logger.LogInformation("DeveloperApplication record created for {Email}", effectiveEmail); } else { var appError = await appResponse.Content.ReadAsStringAsync(); _logger.LogWarning("Failed to create DeveloperApplication record for {Email}: {StatusCode} - {Body}", effectiveEmail, appResponse.StatusCode, appError); } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to create DeveloperApplication record for {Email} — user and ticket were created successfully", effectiveEmail); } _logger.LogInformation("Developer application submitted for {Email} as {Role} — user registered and ticket created", effectiveEmail, application.Role); return (true, "Application submitted successfully! Your SilverDESK account has been created.", token); } catch (Exception ex) { _logger.LogError(ex, "Error submitting developer application for {Username}", application.DesiredUsername); return (false, "Unable to connect to the application service. Please try again later.", null); } } /// /// Serializes structured assessment data as JSON for the Skills column. /// internal static string SerializeAssessment(DeveloperApplication app) { object data; if (app.Role == ApplicationRole.Tester) { data = new { type = "tester", internetUnderstanding = app.InternetUnderstanding ?? 0, enjoysTesting = app.EnjoysTesting ?? 0, additionalNotes = app.AdditionalNotes ?? "" }; } else { data = new { type = "developer", experienceRange = app.ExperienceRange ?? "", selectedSkills = app.SelectedSkills, additionalNotes = app.AdditionalNotes ?? "" }; } return JsonSerializer.Serialize(data, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); } /// /// Generates a human-readable summary for the Motivation field (backward compatibility). /// internal static string GenerateMotivationSummary(DeveloperApplication app) { if (app.Role == ApplicationRole.Tester) { var summary = $"Internet understanding: {app.InternetUnderstanding}/5, Testing enthusiasm: {app.EnjoysTesting}/5"; if (!string.IsNullOrWhiteSpace(app.AdditionalNotes)) summary += $". Notes: {app.AdditionalNotes.Trim()}"; return summary; } else { var skills = app.SelectedSkills.Count > 0 ? string.Join(", ", app.SelectedSkills) : "None selected"; var summary = $"{app.ExperienceRange} experience. Skills: {skills}"; if (!string.IsNullOrWhiteSpace(app.AdditionalNotes)) summary += $". Notes: {app.AdditionalNotes.Trim()}"; return summary; } } private static string ParseRegistrationError(string errorBody) { try { var error = JsonSerializer.Deserialize(errorBody); if (error.TryGetProperty("message", out var message)) { var msg = message.GetString() ?? ""; if (msg.Contains("Username already exists", StringComparison.OrdinalIgnoreCase)) return "That username is already taken. Please choose a different one."; if (msg.Contains("Email already exists", StringComparison.OrdinalIgnoreCase)) return "An account with that email already exists."; if (msg.Contains("Password", StringComparison.OrdinalIgnoreCase)) return msg; return msg; } } catch { } return "Something went wrong creating your account. Please try again later."; } private static string FormatTicketBody(DeveloperApplication app) { var effectiveEmail = string.IsNullOrWhiteSpace(app.Email) ? $"{app.DesiredUsername}@silverlabs.uk" : app.Email.Trim(); var sb = new StringBuilder(); sb.AppendLine("## Developer Program Application"); sb.AppendLine(); sb.AppendLine($"**Role:** {app.Role}"); sb.AppendLine($"**Full Name:** {app.FullName}"); sb.AppendLine($"**Email:** {effectiveEmail}"); sb.AppendLine($"**Desired Username:** {app.DesiredUsername}"); sb.AppendLine($"**Timezone:** {app.Timezone}"); sb.AppendLine(); sb.AppendLine($"**Platforms:** {string.Join(", ", app.Platforms)}"); sb.AppendLine(); if (app.Role == ApplicationRole.Tester) { sb.AppendLine("### Assessment"); sb.AppendLine($"- Internet understanding: {"*".PadLeft(app.InternetUnderstanding ?? 0, '*')}{new string('-', 5 - (app.InternetUnderstanding ?? 0))} ({app.InternetUnderstanding}/5)"); sb.AppendLine($"- Testing enthusiasm: {"*".PadLeft(app.EnjoysTesting ?? 0, '*')}{new string('-', 5 - (app.EnjoysTesting ?? 0))} ({app.EnjoysTesting}/5)"); } else { sb.AppendLine("### Skills & Experience"); sb.AppendLine($"**Experience:** {app.ExperienceRange}"); sb.AppendLine(); if (app.SelectedSkills.Count > 0) { sb.AppendLine($"**Technologies:** {string.Join(", ", app.SelectedSkills)}"); } } if (!string.IsNullOrWhiteSpace(app.AdditionalNotes)) { sb.AppendLine(); sb.AppendLine("### Additional Notes"); sb.AppendLine(app.AdditionalNotes.Trim()); } return sb.ToString(); } }