@page "/"
@rendermode InteractiveServer
@using System
@inject LookupService LookupService

<PageTitle>Lookup Desk</PageTitle>

@if (referenceData is null)
{
    <section class="panel">
        <p class="muted">Loading tables and options...</p>
    </section>
}
else
{
    <div class="dashboard-grid">
        <section class="panel">
            <h2 class="panel-title">Attack Lookup</h2>
            <p class="panel-copy">Choose an attack, armor type, and attack roll. If it produces a critical and you enter the follow-up roll, the app resolves that too.</p>

            <div class="lookup-form">
                <div class="form-grid">
                    <div class="field-shell">
                        <label for="attack-table">Attack table</label>
                        <select id="attack-table" class="input-shell" value="@attackInput.AttackTable" @onchange="HandleAttackTableChanged">
                            @foreach (var attackTable in referenceData.AttackTables)
                            {
                                <option value="@attackTable.Key">@attackTable.Label</option>
                            }
                        </select>
                    </div>

                    <div class="field-shell">
                        <label for="armor-type">Armor type</label>
                        <select id="armor-type" class="input-shell" @bind="attackInput.ArmorType" @bind:after="HandleAttackInputsChanged">
                            @foreach (var armorType in referenceData.ArmorTypes)
                            {
                                <option value="@armorType.Key">@armorType.Label</option>
                            }
                        </select>
                    </div>

                    <div class="field-shell">
                        <label for="attack-roll">Attack roll</label>
                        <div class="roll-input-row">
                            <input id="attack-roll" class="input-shell" type="number" min="1" @bind="attackInput.AttackRoll" @bind:after="HandleAttackInputsChanged" />
                            <button type="button" class="roll-button" @onclick="RollAttack">Roll</button>
                        </div>
                        @if (SelectedAttackTable is { FumbleMinRoll: int attackFumbleMin, FumbleMaxRoll: int attackFumbleMax })
                        {
                            <p class="muted lookup-roll-note">Fumble range: @FormatRange(attackFumbleMin, attackFumbleMax)</p>
                        }
                        @if (!string.IsNullOrWhiteSpace(attackRollSummary))
                        {
                            <p class="muted lookup-roll-note">@attackRollSummary</p>
                        }
                        @if (!string.IsNullOrWhiteSpace(attackFumbleMessage))
                        {
                            <p class="lookup-roll-note is-warning">@attackFumbleMessage</p>
                        }
                    </div>

                    <div class="field-shell">
                        <label for="attack-ob">OB</label>
                        <input id="attack-ob" class="input-shell" type="number" @bind="attackInput.OffensiveBonus" @bind:after="HandleAttackInputsChanged" />
                    </div>

                    <div class="field-shell">
                        <label for="attack-db">DB</label>
                        <input id="attack-db" class="input-shell" type="number" @bind="attackInput.DefensiveBonus" @bind:after="HandleAttackInputsChanged" />
                    </div>

                    <div class="field-shell">
                        <label for="critical-roll">Critical roll</label>
                        <div class="roll-input-row">
                            <input id="critical-roll" class="input-shell" type="number" min="1" @bind="attackInput.CriticalRollText" />
                            <button type="button" class="roll-button" @onclick="RollAttackCriticalAsync">Roll</button>
                        </div>
                        @if (!string.IsNullOrWhiteSpace(attackCriticalRollSummary))
                        {
                            <p class="muted lookup-roll-note">@attackCriticalRollSummary</p>
                        }
                    </div>
                </div>

                <div class="action-row">
                    <button class="btn-ritual" @onclick="RunAttackLookupAsync">Resolve attack</button>
                    <span class="muted">Leave critical roll blank if you only need the hit result.</span>
                </div>
            </div>

            @if (!string.IsNullOrWhiteSpace(attackError))
            {
                <p class="error-text">@attackError</p>
            }

            @if (attackResult is not null)
            {
                <div class="result-shell">
                    <div class="result-card">
                        <h3>@attackResult.AttackTableName vs @attackResult.ArmorTypeLabel</h3>
                        <div class="result-stats">
                            @if (lastAttackResolution is not null)
                            {
                                <span class="stat-pill">Rolled: @lastAttackResolution.RawRoll</span>
                                @if (lastAttackResolution.OffensiveBonus != 0)
                                {
                                    <span class="stat-pill">OB: +@lastAttackResolution.OffensiveBonus</span>
                                }
                                @if (lastAttackResolution.DefensiveBonus != 0)
                                {
                                    <span class="stat-pill">DB: -@lastAttackResolution.DefensiveBonus</span>
                                }
                                <span class="stat-pill">Total used: @lastAttackResolution.EffectiveTotal</span>
                            }
                            else
                            {
                                <span class="stat-pill">Attack total: @attackResult.Roll</span>
                            }
                            <span class="stat-pill">Hits: @attackResult.Hits</span>
                            @if (!string.IsNullOrWhiteSpace(attackResult.CriticalSeverity))
                            {
                                <span class="stat-pill">@attackResult.CriticalSeverity @attackResult.CriticalType critical</span>
                            }
                            else
                            {
                                <span class="stat-pill">No critical</span>
                            }
                        </div>
                        @if (!string.IsNullOrWhiteSpace(attackResult.Notes))
                        {
                            <p class="muted">@attackResult.Notes</p>
                        }

                        @if (attackResult.AutoCritical is not null)
                        {
                            <div class="callout">
                                <h4>Resolved critical</h4>
                                <CriticalLookupResultCard Result="attackResult.AutoCritical" />
                            </div>
                        }
                        else if (!string.IsNullOrWhiteSpace(attackResult.CriticalSeverity))
                        {
                            <div class="callout">The attack produced a critical. Add a critical roll to resolve it automatically.</div>
                        }
                    </div>
                </div>
            }
        </section>

        <section class="panel">
            <h2 class="panel-title">Direct Critical Lookup</h2>
            <p class="panel-copy">Use this when you already know the critical table, severity, roll, and variant if the table uses one.</p>

            <div class="lookup-form">
                <div class="form-grid">
                    <div class="field-shell">
                        <label for="critical-table">Critical table</label>
                        <select id="critical-table" class="input-shell" value="@criticalInput.CriticalType" @onchange="HandleCriticalTableChanged">
                            @foreach (var criticalTable in referenceData.CriticalTables)
                            {
                                <option value="@criticalTable.Key">@criticalTable.Label</option>
                            }
                        </select>
                    </div>

                    <div class="field-shell">
                        <label for="critical-column">Severity</label>
                        <select id="critical-column" class="input-shell" @bind="criticalInput.Column">
                            @foreach (var column in SelectedCriticalTable?.Columns ?? [])
                            {
                                <option value="@column.Key">@column.Label</option>
                            }
                        </select>
                    </div>

                    @if (SelectedCriticalTable?.Groups.Count > 0)
                    {
                        <div class="field-shell">
                            <label for="critical-group">Variant</label>
                            <select id="critical-group" class="input-shell" @bind="criticalInput.Group">
                                @foreach (var group in SelectedCriticalTable.Groups)
                                {
                                    <option value="@group.Key">@group.Label</option>
                                }
                            </select>
                        </div>
                    }

                    <div class="field-shell">
                        <label for="critical-roll-direct">Critical roll</label>
                        <div class="roll-input-row">
                            <input id="critical-roll-direct" class="input-shell" type="number" min="1" @bind="criticalInput.Roll" />
                            <button type="button" class="roll-button" @onclick="RollDirectCritical">Roll</button>
                        </div>
                        @if (!string.IsNullOrWhiteSpace(directCriticalRollSummary))
                        {
                            <p class="muted lookup-roll-note">@directCriticalRollSummary</p>
                        }
                    </div>
                </div>

                <div class="action-row">
                    <button class="btn-ritual" @onclick="RunCriticalLookupAsync">Resolve critical</button>
                </div>
            </div>

            @if (!string.IsNullOrWhiteSpace(criticalError))
            {
                <p class="error-text">@criticalError</p>
            }

            @if (criticalResult is not null)
            {
                <div class="result-shell">
                    <CriticalLookupResultCard Result="criticalResult" />
                </div>
            }
        </section>
    </div>
}

@code {
    private LookupReferenceData? referenceData;
    private AttackLookupForm attackInput = new();
    private CriticalLookupForm criticalInput = new();
    private AttackLookupResponse? attackResult;
    private CriticalLookupResponse? criticalResult;
    private string? attackError;
    private string? criticalError;
    private string? attackRollSummary;
    private string? attackCriticalRollSummary;
    private string? directCriticalRollSummary;
    private string? attackFumbleMessage;
    private AttackResolutionSummary? lastAttackResolution;

    private AttackTableReference? SelectedAttackTable =>
        referenceData?.AttackTables.FirstOrDefault(table => table.Key == attackInput.AttackTable);

    private CriticalTableReference? SelectedCriticalTable =>
        referenceData?.CriticalTables.FirstOrDefault(table => table.Key == criticalInput.CriticalType);

    protected override async Task OnInitializedAsync()
    {
        referenceData = await LookupService.GetReferenceDataAsync();

        attackInput.AttackTable = referenceData.AttackTables.FirstOrDefault()?.Key ?? string.Empty;
        attackInput.ArmorType = referenceData.ArmorTypes.FirstOrDefault()?.Key ?? string.Empty;

        var initialCriticalTable = referenceData.CriticalTables.FirstOrDefault();
        criticalInput.CriticalType = initialCriticalTable?.Key ?? string.Empty;
        criticalInput.Column = initialCriticalTable?.Columns.FirstOrDefault()?.Key ?? string.Empty;
        criticalInput.Group = initialCriticalTable?.Groups.FirstOrDefault()?.Key ?? string.Empty;
    }

    private async Task RunAttackLookupAsync()
    {
        attackError = null;
        attackResult = null;
        var attackResolution = BuildAttackResolution();
        attackFumbleMessage = BuildAttackFumbleMessage(attackResolution);

        if (!string.IsNullOrWhiteSpace(attackFumbleMessage))
        {
            return;
        }

        if (!int.TryParse(attackInput.CriticalRollText, out var criticalRoll) && !string.IsNullOrWhiteSpace(attackInput.CriticalRollText))
        {
            attackError = "Critical roll must be empty or a whole number.";
            return;
        }

        var response = await LookupService.LookupAttackAsync(new AttackLookupRequest(
            attackInput.AttackTable,
            attackInput.ArmorType,
            attackResolution.EffectiveTotal,
            string.IsNullOrWhiteSpace(attackInput.CriticalRollText) ? null : criticalRoll));

        if (response is null)
        {
            attackError = "No attack result matched that table, armor type, and roll.";
            return;
        }

        attackResult = response;
        lastAttackResolution = attackResolution;
    }

    private async Task RunCriticalLookupAsync()
    {
        criticalError = null;
        criticalResult = null;

        var response = await LookupService.LookupCriticalAsync(new CriticalLookupRequest(
            criticalInput.CriticalType,
            criticalInput.Column,
            criticalInput.Roll,
            SelectedCriticalTable?.Groups.Count > 0 ? criticalInput.Group : null));

        if (response is null)
        {
            criticalError = "No critical result matched that selection and roll.";
            return;
        }

        criticalResult = response;
    }

    private void HandleCriticalTableChanged(ChangeEventArgs args)
    {
        criticalInput.CriticalType = args.Value?.ToString() ?? string.Empty;

        var table = referenceData?.CriticalTables.FirstOrDefault(item => item.Key == criticalInput.CriticalType);
        criticalInput.Column = table?.Columns.FirstOrDefault()?.Key ?? string.Empty;
        criticalInput.Group = table?.Groups.FirstOrDefault()?.Key ?? string.Empty;
        criticalResult = null;
        criticalError = null;
        directCriticalRollSummary = null;
    }

    private void HandleAttackTableChanged(ChangeEventArgs args)
    {
        attackInput.AttackTable = args.Value?.ToString() ?? string.Empty;
        HandleAttackInputsChanged();
    }

    private void RollAttack()
    {
        var result = RolemasterRoller.RollAttack(Random.Shared);
        attackInput.AttackRoll = result.Total;
        attackRollSummary = BuildAttackRollSummary(result, BuildAttackResolution());
        HandleAttackInputsChanged(clearRollSummary: false);
    }

    private async Task RollAttackCriticalAsync()
    {
        attackError = null;
        attackCriticalRollSummary = null;
        var attackResolution = BuildAttackResolution();
        attackFumbleMessage = BuildAttackFumbleMessage(attackResolution);

        if (!string.IsNullOrWhiteSpace(attackFumbleMessage))
        {
            attackCriticalRollSummary = "No critical roll is needed because the current attack roll is a fumble.";
            return;
        }

        CriticalTableReference? criticalTable = null;
        var pendingAttack = await LookupService.LookupAttackAsync(new AttackLookupRequest(
            attackInput.AttackTable,
            attackInput.ArmorType,
            attackResolution.EffectiveTotal,
            null));

        if (!string.IsNullOrWhiteSpace(pendingAttack?.CriticalType))
        {
            criticalTable = referenceData?.CriticalTables.FirstOrDefault(item =>
                string.Equals(item.Key, pendingAttack.CriticalType, StringComparison.Ordinal));
        }

        var result = RolemasterRoller.RollCritical(Random.Shared, criticalTable);
        attackInput.CriticalRollText = result.Total.ToString();
        attackCriticalRollSummary = BuildCriticalRollSummary(result, criticalTable);
        attackResult = null;
    }

    private void RollDirectCritical()
    {
        var result = RolemasterRoller.RollCritical(Random.Shared, SelectedCriticalTable);
        criticalInput.Roll = result.Total;
        directCriticalRollSummary = BuildCriticalRollSummary(result, SelectedCriticalTable);
        criticalResult = null;
        criticalError = null;
    }

    private void HandleAttackInputsChanged()
    {
        HandleAttackInputsChanged(clearRollSummary: true);
    }

    private void HandleAttackInputsChanged(bool clearRollSummary)
    {
        if (clearRollSummary)
        {
            attackRollSummary = null;
        }

        attackResult = null;
        attackError = null;
        attackCriticalRollSummary = null;
        lastAttackResolution = null;
        attackFumbleMessage = BuildAttackFumbleMessage(BuildAttackResolution());
    }

    private AttackResolutionSummary BuildAttackResolution() =>
        AttackResolutionCalculator.Resolve(
            attackInput.AttackRoll,
            attackInput.OffensiveBonus,
            attackInput.DefensiveBonus);

    private string? BuildAttackFumbleMessage(AttackResolutionSummary resolution)
    {
        if (!AttackResolutionCalculator.IsFumble(resolution, SelectedAttackTable) ||
            SelectedAttackTable is not { FumbleMinRoll: int fumbleMinRoll, FumbleMaxRoll: int fumbleMaxRoll })
        {
            return null;
        }

        return $"{SelectedAttackTable.Label} fumble on {FormatRange(fumbleMinRoll, fumbleMaxRoll)}. Raw roll {resolution.RawRoll:00} triggers the fumble before OB or DB is applied.";
    }

    private static string BuildRollSummary(LookupRollResult result, string label)
    {
        if (!result.IsOpenEnded)
        {
            return $"{label} roll: {result.Total}.";
        }

        return $"{label} roll: {string.Join(" + ", result.Rolls)} = {result.Total}.";
    }

    private static string BuildAttackRollSummary(LookupRollResult result, AttackResolutionSummary resolution)
    {
        var summary = BuildRollSummary(result, "Attack");
        if (resolution.OffensiveBonus == 0 && resolution.DefensiveBonus == 0)
        {
            return summary;
        }

        var modifiers = new List<string>();
        if (resolution.OffensiveBonus != 0)
        {
            modifiers.Add($"+{resolution.OffensiveBonus} OB");
        }

        if (resolution.DefensiveBonus != 0)
        {
            modifiers.Add($"-{resolution.DefensiveBonus} DB");
        }

        return $"{summary} {string.Join(' ', modifiers)} = {resolution.EffectiveTotal}.";
    }

    private static string BuildCriticalRollSummary(
        LookupRollResult result,
        CriticalTableReference? criticalTable)
    {
        var summary = BuildRollSummary(result, "Critical");
        if (criticalTable is null)
        {
            return $"{summary} Standard 1-100 roll used because no critical table is currently resolved from the attack result.";
        }

        return RolemasterRoller.AllowsOpenEndedCritical(criticalTable)
            ? $"{summary} {criticalTable.Label} uses open-ended rolls."
            : $"{summary} {criticalTable.Label} is capped at 1-100.";
    }

    private static string FormatRange(int minRoll, int maxRoll) =>
        $"{minRoll:00}-{maxRoll:00}";

    private sealed class AttackLookupForm
    {
        public string AttackTable { get; set; } = string.Empty;
        public string ArmorType { get; set; } = string.Empty;
        public int AttackRoll { get; set; } = 66;
        public int OffensiveBonus { get; set; }
        public int DefensiveBonus { get; set; }
        public string? CriticalRollText { get; set; } = "72";
    }

    private sealed class CriticalLookupForm
    {
        public string CriticalType { get; set; } = string.Empty;
        public string Column { get; set; } = string.Empty;
        public string Group { get; set; } = string.Empty;
        public int Roll { get; set; } = 72;
    }
}
