using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using ROLAC.API.Data;
using ROLAC.API.DTOs.Expense;

namespace ROLAC.API.Services.Ai;

/// <summary>
/// Provider-independent category-AI logic: loads the active Form 990 line catalog, builds the
/// mapping prompt, and validates the model's chosen line id against that catalog. Concrete providers
/// (Gemini, Claude) only implement <see cref="CallModelAsync"/> — the HTTP call plus response parsing —
/// so the catalog/prompt/validation code lives in exactly one place. Mirrors
/// <see cref="ExpenseAiServiceBase"/>, which does the same for the expense-entry classification task.
/// </summary>
public abstract class ExpenseCategoryAiServiceBase : IExpenseCategoryAiService
{
    private readonly AppDbContext _db;

    protected ExpenseCategoryAiServiceBase(AppDbContext db) => _db = db;

    /// <summary>One Form 990 line in the catalog passed to the model.</summary>
    protected sealed record CatalogLine(int Id, string LineCode, string NameEn, string? NameZh);

    /// <summary>The model's raw answer, before its line id is validated against the catalog.</summary>
    protected sealed record ModelAnswer(string? ChineseName, string? EnglishName, int? Form990LineId, double Confidence);

    public async Task<CategoryAiSuggestion> SuggestAsync(ExpenseCategoryAiRequest request, CancellationToken ct = default)
    {
        var catalog = await LoadCatalogAsync(ct);
        var prompt = BuildPrompt(request, catalog);

        var answer = await CallModelAsync(prompt, ct);
        if (answer is null) return new CategoryAiSuggestion();

        return BuildSuggestion(answer, catalog);
    }

    /// <summary>
    /// Call the provider's API with <paramref name="prompt"/> and return its parsed answer, or null
    /// on any failure (missing key, HTTP error, unparseable response). Implementations must not throw.
    /// </summary>
    protected abstract Task<ModelAnswer?> CallModelAsync(string prompt, CancellationToken ct);

    private async Task<List<CatalogLine>> LoadCatalogAsync(CancellationToken ct)
    {
        return await _db.Form990ExpenseLines
            .AsNoTracking()
            .Where(line => line.IsActive)
            .OrderBy(line => line.SortOrder)
            .Select(line => new CatalogLine(line.Id, line.LineCode, line.Name_en, line.Name_zh))
            .ToListAsync(ct);
    }

    private static string BuildPrompt(ExpenseCategoryAiRequest request, List<CatalogLine> catalog)
    {
        var catalogJson = JsonSerializer.Serialize(catalog);
        var levelLabel = request.Level.Equals("sub", StringComparison.OrdinalIgnoreCase)
            ? "sub-category (小項)"
            : "major category (大項)";

        var context = new StringBuilder();
        context.Append($"This is an expense {levelLabel} in a church's bookkeeping chart of accounts.\n");
        if (!string.IsNullOrWhiteSpace(request.Name_zh))
            context.Append($"Chinese name entered: {request.Name_zh}\n");
        if (!string.IsNullOrWhiteSpace(request.Name_en))
            context.Append($"English name entered: {request.Name_en}\n");
        if (!string.IsNullOrWhiteSpace(request.ParentGroupName))
            context.Append($"It belongs under the parent major category: {request.ParentGroupName}\n");
        if (request.ParentForm990LineId is int parentLineId)
            context.Append(
                $"The parent major category is mapped to Form 990 line id {parentLineId}; prefer a consistent " +
                "choice unless a more specific line clearly fits this sub-category.\n");

        return
            "You are a bookkeeping assistant for a church mapping its expense categories to the IRS Form 990 " +
            "Part IX (Statement of Functional Expenses) lines. Given an expense category name (often in " +
            "Traditional Chinese), do three things:\n" +
            "1. Correct any typos in the name and refine it into natural Traditional Chinese — return it as " +
            "chineseName.\n" +
            "2. Translate that into a concise, natural accounting English noun phrase (not a full sentence) — " +
            "return it as englishName.\n" +
            "3. Choose the single best matching Form 990 line from the catalog below. You MUST pick a " +
            "form990LineId that appears in the catalog. If nothing fits well, choose the closest general line " +
            "(e.g. an \"Other expenses\" line) and lower your confidence.\n\n" +
            context +
            "\n" +
            $"Form 990 line catalog (JSON; each line has an Id, LineCode, and English/Chinese names):\n{catalogJson}";
    }

    private static CategoryAiSuggestion BuildSuggestion(ModelAnswer answer, List<CatalogLine> catalog)
    {
        var suggestion = new CategoryAiSuggestion
        {
            ChineseName = string.IsNullOrWhiteSpace(answer.ChineseName) ? null : answer.ChineseName.Trim(),
            EnglishName = string.IsNullOrWhiteSpace(answer.EnglishName) ? null : answer.EnglishName.Trim(),
            Confidence = answer.Confidence,
        };

        // Re-validate the returned id against the catalog; drop a hallucinated id rather than returning it.
        var line = catalog.FirstOrDefault(candidate => candidate.Id == answer.Form990LineId);
        if (line is not null)
        {
            suggestion.Form990LineId = line.Id;
            suggestion.Form990LineLabel = Label(line);
        }

        return suggestion;
    }

    /// <summary>Mirror the frontend dropdown label: "code — English / 中文" (or just "code — English").</summary>
    private static string Label(CatalogLine line)
        => string.IsNullOrWhiteSpace(line.NameZh)
            ? $"{line.LineCode} — {line.NameEn}"
            : $"{line.LineCode} — {line.NameEn} / {line.NameZh}";
}
