using System.Text.Json;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using Microsoft.EntityFrameworkCore;

namespace JobTrackerApi.Services;

public sealed record AiGenerateRequest(
    string Module,
    string? Mode,
    string? ExtraContext,
    string? TargetLanguage = null,
    string? CurrentText = null,
    string? Action = null);

// Thrown when the AI service returns nothing usable — the controller maps it to 502 with the reason.
public sealed class AiUnavailableException : Exception
{
    public AiUnavailableException(string message) : base(message) { }
}

public interface IAiWorkspaceService
{
    // Runs one module, stores the result as an append-only AiInteraction, and returns it. Never
    // mutates the profile, a CV variant, or the application — suggestion only.
    Task<AiInteraction?> GenerateAsync(string ownerUserId, int jobApplicationId, string profileText, string candidateName, AiGenerateRequest req, string provider, CancellationToken ct);
    Task<IReadOnlyList<AiInteraction>> HistoryAsync(string ownerUserId, int jobApplicationId, string? module, CancellationToken ct);
    Task<AiInteraction?> GetAsync(string ownerUserId, int id, CancellationToken ct);
    Task<bool> DeleteAsync(string ownerUserId, int id, CancellationToken ct);

    // The module keys this service supports (for the controller/UI to enumerate).
    IReadOnlyList<string> Modules { get; }
}

public sealed class AiWorkspaceService : IAiWorkspaceService
{
    private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);

    public IReadOnlyList<string> Modules { get; } = new[]
    {
        "job-analysis", "career-match", "cover-letter", "interview", "application-review",
    };

    private static readonly HashSet<string> CoverLetterModes = new(StringComparer.OrdinalIgnoreCase)
    {
        "professional", "friendly", "short", "detailed", "modern", "traditional",
    };

    private const string Guardrail =
        "Preserve every factual claim — never invent employers, titles, dates, qualifications, technologies, achievements, years of experience, language ability, or metrics. "
        + "Do not claim enthusiasm for products the source does not show the candidate has used. Avoid corporate clichés and do not repeat the CV or advert verbatim. "
        + "This is a suggestion the user will review and edit; return only the requested content, in clean markdown, with no preamble.";

    private readonly JobTrackerContext _db;
    private readonly ISummarizerService _ai;
    private readonly IApplicationIntelligenceService? _intelligence;

    // Optional on purpose: every existing construction of this service keeps working unchanged, and a
    // missing intelligence service degrades to the previous prompt rather than failing generation.
    public AiWorkspaceService(JobTrackerContext db, ISummarizerService ai, IApplicationIntelligenceService? intelligence = null)
    {
        _db = db;
        _ai = ai;
        _intelligence = intelligence;
    }

    // The deterministic workspace output, formatted for the prompt. Read-only: AnalyzeAsync and
    // MatchAsync own no data and write nothing, so this cannot touch the profile or the application.
    private async Task<string> BuildIntelligenceContextAsync(string ownerUserId, int jobApplicationId, CancellationToken ct)
    {
        var analysis = await _intelligence!.AnalyzeAsync(ownerUserId, jobApplicationId, ct);
        var match = await _intelligence.MatchAsync(ownerUserId, jobApplicationId, ct);
        if (analysis is null && match is null) return string.Empty;

        var sb = new System.Text.StringBuilder();
        sb.Append("\n\nAPPLICATION INTELLIGENCE (already computed — use it, do not restate it):");

        if (analysis is not null)
        {
            Line(sb, "Seniority", analysis.Seniority);
            Line(sb, "Employment type", analysis.EmploymentType);
            List(sb, "Key requirements", analysis.ImportantRequirements);
            List(sb, "Technologies in the advert", analysis.Technologies);
        }

        if (match is { HasCareerProfile: true })
        {
            sb.Append($"\nMatch score: {match.Score}% ({match.Band})");
            List(sb, "Skills the candidate demonstrably has", match.MatchedSkills);
            // The gaps are the point: this is where an interviewer will probe.
            List(sb, "Gaps the candidate must be ready to address", match.MissingSkills);
            List(sb, "Most relevant experience",
                match.RelevantExperience.Select(e => e.Subtitle is null ? e.Title : $"{e.Title} ({e.Subtitle})").ToList());
            List(sb, "Most relevant projects", match.RelevantProjects.Select(p => p.Title).ToList());
        }

        return sb.ToString();

        static void Line(System.Text.StringBuilder sb, string label, string? value)
        {
            if (!string.IsNullOrWhiteSpace(value)) sb.Append($"\n{label}: {value}");
        }

        static void List(System.Text.StringBuilder sb, string label, IReadOnlyList<string> values)
        {
            if (values.Count == 0) return;
            sb.Append($"\n{label}: {string.Join("; ", values.Take(8))}");
        }
    }

    public async Task<AiInteraction?> GenerateAsync(string ownerUserId, int jobApplicationId, string profileText, string candidateName, AiGenerateRequest req, string provider, CancellationToken ct)
    {
        var module = (req.Module ?? string.Empty).Trim().ToLowerInvariant();
        if (!Modules.Contains(module)) throw new ArgumentException($"Unknown AI module '{module}'.");

        var job = await _db.JobApplications.Include(j => j.Company)
            .FirstOrDefaultAsync(j => j.Id == jobApplicationId && j.OwnerUserId == ownerUserId, ct);
        if (job is null) return null;

        var jobText = BuildJobContext(job);
        var linkedCv = module is "cover-letter" or "career-match" or "interview"
            ? await BuildLinkedCvContextAsync(ownerUserId, jobApplicationId, candidateName, ct)
            : null;
        var profile = linkedCv?.Text ?? (string.IsNullOrWhiteSpace(profileText) ? "(no candidate profile on file yet)" : profileText.Trim());
        var mode = NormalizeMode(module, req.Mode);
        var extra = string.IsNullOrWhiteSpace(req.ExtraContext) ? string.Empty : $"\n\nAdditional user context:\n{req.ExtraContext.Trim()}";
        var language = NormalizeLanguage(req.TargetLanguage);
        var languageInstruction = language == "nb-NO"
            ? "Write in natural, professional Norwegian Bokmål (nb-NO)."
            : "Write in natural, professional English.";
        var currentDraft = string.IsNullOrWhiteSpace(req.CurrentText) ? string.Empty : $"\n\nCURRENT DOCUMENT TO REVISE:\n{req.CurrentText.Trim()}";
        var action = NormalizeAction(req.Action);

        // Interview prep is the module that benefits most from what the workspace already computed:
        // asking for likely questions without the requirements, the matched skills and — above all —
        // the gaps produces generic output. Everything here is deterministic and already on screen, so
        // this adds context, not another AI call. Null when unavailable, and the prompt is unchanged.
        var intelligence = module is "interview" or "cover-letter" && _intelligence is not null
            ? await BuildIntelligenceContextAsync(ownerUserId, jobApplicationId, ct)
            : string.Empty;

        var (instruction, source, title, max) = module switch
        {
            "job-analysis" => (JobAnalysisPrompt(), jobText + extra, "Job analysis", 1000),
            "career-match" => (CareerMatchPrompt(), $"CANDIDATE PROFILE:\n{profile}\n\nJOB ADVERT:\n{jobText}{extra}", "Career match", 1000),
            "cover-letter" => (CoverLetterPrompt(mode!, candidateName, action, languageInstruction), $"CV BEING USED: {linkedCv?.Name ?? "Career profile"}\n{profile}\n\nJOB ADVERT:\n{jobText}{intelligence}{currentDraft}{extra}", $"Cover letter · {Capitalize(action)} · {Capitalize(mode!)}", 900),
            "interview" => ($"{InterviewPrompt()} {languageInstruction}", $"CV BEING USED: {linkedCv?.Name ?? "Career profile"}\n{profile}\n\nJOB ADVERT:\n{jobText}{intelligence}{extra}", "Interview prep", 1100),
            "application-review" => (ApplicationReviewPrompt(), $"CANDIDATE PROFILE:\n{profile}\n\nJOB ADVERT:\n{jobText}{extra}", "Application review", 900),
            _ => throw new ArgumentException($"Unknown AI module '{module}'."),
        };

        var prompt = $"{instruction} {Guardrail}";
        AiGenerationResult? generation;
        try
        {
            generation = await _ai.GenerateSectionWithMetadataAsync(prompt, source, max, 120, ct);
        }
        catch (AiGenerationException)
        {
            throw new AiUnavailableException("The AI service could not generate this right now. Please try again in a moment.");
        }
        var result = generation?.Text;
        if (string.IsNullOrWhiteSpace(result))
        {
            throw new AiUnavailableException("The AI service could not generate this right now. Please try again in a moment.");
        }

        var actualProvider = string.IsNullOrWhiteSpace(generation?.Provider) ? provider : generation.Provider;

        var interaction = new AiInteraction
        {
            OwnerUserId = ownerUserId,
            JobApplicationId = jobApplicationId,
            Module = module,
            Mode = mode,
            Title = title,
            Provider = string.IsNullOrWhiteSpace(actualProvider) ? "ai-service" : actualProvider,
            ResultJson = JsonSerializer.Serialize(new
            {
                text = result.Trim(),
                meta = new
                {
                    model = generation?.Model,
                    fallbackReason = generation?.FallbackReason,
                    routeReason = generation?.RouteReason,
                },
            }, Json),
            InputCharacterCount = prompt.Length + source.Length,
            OutputCharacterCount = result.Trim().Length,
            EstimatedTokenCount = EstimateTokens(prompt.Length + source.Length + result.Trim().Length),
            CreatedAtUtc = DateTimeOffset.UtcNow,
        };
        _db.AiInteractions.Add(interaction);
        await _db.SaveChangesAsync(ct);
        return interaction;
    }

    public async Task<IReadOnlyList<AiInteraction>> HistoryAsync(string ownerUserId, int jobApplicationId, string? module, CancellationToken ct)
    {
        var q = _db.AiInteractions.Where(x => x.OwnerUserId == ownerUserId && x.JobApplicationId == jobApplicationId);
        if (!string.IsNullOrWhiteSpace(module)) { var m = module.Trim().ToLowerInvariant(); q = q.Where(x => x.Module == m); }
        return _db.Database.IsSqlite()
            ? (await q.ToListAsync(ct)).OrderByDescending(x => x.CreatedAtUtc).ToList()
            : await q.OrderByDescending(x => x.CreatedAtUtc).ToListAsync(ct);
    }

    public Task<AiInteraction?> GetAsync(string ownerUserId, int id, CancellationToken ct) =>
        _db.AiInteractions.FirstOrDefaultAsync(x => x.Id == id && x.OwnerUserId == ownerUserId, ct);

    public async Task<bool> DeleteAsync(string ownerUserId, int id, CancellationToken ct)
    {
        var row = await GetAsync(ownerUserId, id, ct);
        if (row is null) return false;
        _db.AiInteractions.Remove(row);
        await _db.SaveChangesAsync(ct);
        return true;
    }

    internal static int EstimateTokens(int characterCount) => Math.Max(0, (characterCount + 3) / 4);

    private static string? NormalizeMode(string module, string? mode)
    {
        if (module != "cover-letter") return null;
        var m = (mode ?? "professional").Trim().ToLowerInvariant();
        return CoverLetterModes.Contains(m) ? m : "professional";
    }

    private static string NormalizeLanguage(string? language) =>
        string.Equals(language?.Trim(), "nb", StringComparison.OrdinalIgnoreCase)
        || string.Equals(language?.Trim(), "nb-NO", StringComparison.OrdinalIgnoreCase)
            ? "nb-NO"
            : "en";

    private static string NormalizeAction(string? action)
    {
        var value = action?.Trim().ToLowerInvariant();
        return value is "generate" or "regenerate" or "improve" or "shorten" or "expand" or "professional" or "natural" or "grammar" or "tailor"
            ? value
            : "generate";
    }

    private async Task<(string Name, string Text)?> BuildLinkedCvContextAsync(string ownerUserId, int jobApplicationId, string candidateName, CancellationToken ct)
    {
        var query = _db.CvVariants.AsNoTracking()
            .Where(v => v.OwnerUserId == ownerUserId && v.JobApplicationId == jobApplicationId);
        var variant = _db.Database.IsSqlite()
            ? (await query.ToListAsync(ct)).MaxBy(v => v.UpdatedAtUtc)
            : await query.OrderByDescending(v => v.UpdatedAtUtc).FirstOrDefaultAsync(ct);
        if (variant is null) return null;

        var profileJson = await _db.CareerProfiles.AsNoTracking()
            .Where(profile => profile.OwnerUserId == ownerUserId)
            .Select(profile => profile.ProfileJson)
            .FirstOrDefaultAsync(ct);
        if (string.IsNullOrWhiteSpace(profileJson)) return (variant.Name, "(the linked CV has no content yet)");

        var profile = StructuredCvProfileJson.DeserializePersisted(profileJson);
        var model = CvVariantResolver.Build(profile, CvVariantSettingsJson.Deserialize(variant.SettingsJson), candidateName, null);
        var lines = new List<string>();
        foreach (var section in model.Sections)
        {
            lines.Add($"## {section.Title}");
            lines.AddRange(section.Bullets);
            lines.AddRange(section.Tags);
            lines.AddRange(section.SkillGroups.Select(group => $"{group.Name}: {string.Join(", ", group.Items)}"));
            lines.AddRange(section.Entries.Select(entry =>
                string.Join(" | ", new[] { entry.Title, entry.Subtitle, entry.Meta, string.Join("; ", entry.Bullets), string.Join(", ", entry.Tags) }
                    .Where(value => !string.IsNullOrWhiteSpace(value)))));
        }
        return (variant.Name, string.Join("\n", lines.Where(line => !string.IsNullOrWhiteSpace(line))));
    }

    private static string BuildJobContext(JobApplication job)
    {
        var parts = new[]
        {
            Field("Role", job.JobTitle),
            Field("Company", job.Company?.Name),
            Field("Status", job.Status),
            Field("Summary", job.ShortSummary),
            Field("Description", job.Description),
            Field("Translated description", job.TranslatedDescription),
            Field("Notes", job.Notes),
            Field("URL", job.JobUrl),
        };
        return string.Join("\n", parts.Where(p => p != null));
    }

    private static string? Field(string label, string? value) => string.IsNullOrWhiteSpace(value) ? null : $"{label}: {value.Trim()}";
    private static string Capitalize(string s) => s.Length == 0 ? s : char.ToUpperInvariant(s[0]) + s[1..];

    // --- Prompts. Each asks for markdown with clear sections; the guardrail is appended by the caller. ---

    private static string JobAnalysisPrompt() =>
        "Analyse this job advert. Return markdown with these sections: **Company**, **Role**, **Required skills**, "
        + "**Nice-to-have skills**, **Technologies**, **Experience**, **Education**, **Soft skills**, **Responsibilities**, "
        + "**Salary** (only if stated), **Benefits**, **Work model**, **Visa requirements**, **Language requirements**, "
        + "**Summary** (2–3 sentences), **Likely interview topics**, and **Confidence** (High/Medium/Low with one line on why). "
        + "Omit any field the advert does not mention rather than guessing.";

    private static string CareerMatchPrompt() =>
        "Compare the candidate profile against the job advert. Return markdown with: **Match** (a single percentage with one "
        + "line of reasoning), **Strengths**, **Weaknesses**, **Missing skills**, **Most relevant experience**, and "
        + "**Suggested improvements** (concrete, actionable). Base every point only on what the profile actually shows.";

    private static string CoverLetterPrompt(string mode, string candidateName, string action, string languageInstruction) =>
        $"{CoverLetterAction(action)} for {(string.IsNullOrWhiteSpace(candidateName) ? "the candidate" : candidateName)} for this role in a "
        + $"{ModeGuidance(mode)} {languageInstruction} Connect specific, supported experience to the job's requirements, acknowledge transferable experience honestly, and ground every claim in the linked CV. Return only the letter body.";

    private static string CoverLetterAction(string action) => action switch
    {
        "regenerate" => "Write a fresh alternative cover letter",
        "improve" => "Improve the clarity and impact of the current cover letter",
        "shorten" => "Shorten the current cover letter while retaining its strongest evidence",
        "expand" => "Add useful, supported detail to the current cover letter",
        "professional" => "Make the current cover letter more professional",
        "natural" => "Make the current cover letter sound more natural and human",
        "grammar" => "Correct grammar and awkward phrasing in the current cover letter",
        "tailor" => "Tailor the current cover letter more closely to the job requirements",
        _ => "Write a cover letter",
    };

    private static string ModeGuidance(string mode) => mode switch
    {
        "friendly" => "warm, personable style — approachable but still professional.",
        "short" => "concise style — 3 short paragraphs at most, every sentence earning its place.",
        "detailed" => "thorough style — cover motivation, the strongest matching experience, and fit, without padding.",
        "modern" => "modern, direct style — confident, plain language, no clichés.",
        "traditional" => "traditional, formal style — conventional structure and measured tone.",
        _ => "professional, confident style.",
    };

    private static string InterviewPrompt() =>
        "Create an interview preparation brief in markdown with: **Company research summary** (from the advert only), "
        + "**Likely interview questions**, **Behavioural questions**, **Technical questions**, **Suggested STAR answers** "
        + "(outline Situation/Task/Action/Result using the candidate's real experience), and a **Preparation checklist**.";

    private static string ApplicationReviewPrompt() =>
        "Review this application (candidate profile as the material to be submitted, against the job advert). Return markdown "
        + "with: **Overall strength** (a one-line verdict + rating out of 10), **Missing information**, **Weak areas**, "
        + "**ATS issues** (keywords/formatting that could hurt automated screening), **Grammar & clarity**, and "
        + "**Formatting suggestions**. Be specific and constructive.";
}
