//! Display/rendering for the Schwab audit output. Reconciliation
//! compute lives in `analytics/reconcile/schwab.zig`; re-exported here
//! for back-compat so `audit.zig` / `hygiene.zig` keep using `schwab.*`.

const std = @import("std");
const zfin = @import("../../root.zig");
const cli = @import("../common.zig");
const Money = @import("../../Money.zig");
const analysis = @import("../../analytics/analysis.zig");
const portfolio_mod = @import("../../models/portfolio.zig");
const Date = @import("../../Date.zig");
const common = @import("common.zig");
const reconcile = @import("../../analytics/reconcile.zig");

pub const SchwabAccountComparison = reconcile.SchwabAccountComparison;
pub const compareSchwabSummary = reconcile.compareSchwabSummary;
pub const reconcileCsv = reconcile.reconcileCsv;
pub const reconcileSummary = reconcile.reconcileSummary;
pub const hasSchwabDiscrepancies = reconcile.hasSchwabDiscrepancies;

pub fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, out: *std.Io.Writer) !void {
    try cli.printBold(out, color, "\nSchwab Account Audit", .{});
    try cli.printFg(out, color, cli.CLR_MUTED, "  (brokerage is source of truth)\n", .{});
    try out.print("========================================\n\n", .{});

    // Column headers
    try cli.printFg(out, color, cli.CLR_MUTED, "  {s:<24} {s:>14} {s:>14} {s:>14} {s:>14}\n", .{
        "Account", "PF Cash", "BR Cash", "PF Total", "BR Total",
    });

    var grand_pf: f64 = 0;
    var grand_br: f64 = 0;
    var discrepancy_count: usize = 0;

    for (results) |r| {
        const label = if (r.account_name.len > 0) r.account_name else r.schwab_name;

        var br_cash_buf: [24]u8 = undefined;
        var br_total_buf: [24]u8 = undefined;

        const br_cash_str = if (r.schwab_cash) |c|
            std.fmt.bufPrint(&br_cash_buf, "{f}", .{Money.from(c)}) catch "$?"
        else
            "--";
        const br_total_str = if (r.schwab_total) |t|
            std.fmt.bufPrint(&br_total_buf, "{f}", .{Money.from(t)}) catch "$?"
        else
            "--";

        const cash_ok = if (r.cash_delta) |d| @abs(d) < common.cash_tolerance else true;
        // A value delta is "exact" (no note) under $1, "expected" (muted)
        // when the account holds options or the delta fits the CD band,
        // and a real "warn" otherwise.
        const band = @max(common.value_tolerance, r.cd_allowance);
        const value_exact = if (r.total_delta) |d| @abs(d) < common.value_tolerance else true;
        const value_expected = r.has_options or (if (r.total_delta) |d| @abs(d) <= band else true);
        const value_warn = !value_exact and !value_expected;
        const is_unmapped = r.account_name.len == 0;
        const is_real_mismatch = !cash_ok or is_unmapped or value_warn;

        if (is_real_mismatch) discrepancy_count += 1;

        // Account label
        try out.print("  ", .{});
        if (is_unmapped) {
            try cli.printFg(out, color, cli.CLR_WARNING, "{s:<24}", .{label});
        } else {
            try out.print("{s:<24}", .{label});
        }

        // PF Cash - colored if mismatched (brokerage is truth)
        try out.print(" ", .{});
        if (!cash_ok) {
            const rgb = if (r.cash_delta.? > 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE;
            try cli.printFg(out, color, rgb, "{f}", .{Money.from(r.portfolio_cash).padRight(14)});
        } else {
            try out.print("{f}", .{Money.from(r.portfolio_cash).padRight(14)});
        }

        // BR Cash
        try out.print(" {s:>14}", .{br_cash_str});

        // PF Total - colored only on a real (beyond-allowance) value mismatch
        try out.print(" ", .{});
        if (value_warn) {
            const rgb = if (r.total_delta.? > 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE;
            try cli.printFg(out, color, rgb, "{f}", .{Money.from(r.portfolio_total).padRight(14)});
        } else {
            try out.print("{f}", .{Money.from(r.portfolio_total).padRight(14)});
        }

        // BR Total
        try out.print(" {s:>14}", .{br_total_str});

        // Status
        if (is_unmapped) {
            try cli.printFg(out, color, cli.CLR_WARNING, " Unmapped", .{});
        } else if (!cash_ok) {
            const d = r.cash_delta.?;
            const sign: []const u8 = if (d >= 0) "+" else "-";
            try cli.printFg(out, color, cli.CLR_WARNING, " Cash {s}{f}", .{ sign, Money.from(@abs(d)) });
        } else if (value_warn) {
            const d = r.total_delta.?;
            const sign: []const u8 = if (d >= 0) "+" else "-";
            try cli.printFg(out, color, cli.CLR_WARNING, " Value {s}{f}", .{ sign, Money.from(@abs(d)) });
        } else if (!value_exact) {
            // Expected difference (CD mark or options present): shown so
            // nothing is hidden, but muted - you probably don't care.
            const d = r.total_delta.?;
            const sign: []const u8 = if (d >= 0) "+" else "-";
            try cli.printFg(out, color, cli.CLR_MUTED, " Value {s}{f}", .{ sign, Money.from(@abs(d)) });
        }
        try out.print("\n", .{});

        grand_pf += r.portfolio_total;
        if (r.schwab_total) |t| grand_br += t;
    }

    // Grand totals
    try out.print("\n", .{});
    const grand_delta = grand_br - grand_pf;

    try cli.printBold(out, color, "  Total: portfolio {f}  schwab {f}", .{
        Money.from(grand_pf),
        Money.from(grand_br),
    });

    if (@abs(grand_delta) < 1.0) {
        // no delta
    } else {
        const sign: []const u8 = if (grand_delta >= 0) "+" else "-";
        const rgb = if (grand_delta >= 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE;
        try cli.printFg(out, color, rgb, "  delta {s}{f}", .{ sign, Money.from(@abs(grand_delta)) });
    }
    try out.print("\n", .{});

    if (discrepancy_count > 0) {
        try cli.printFg(out, color, cli.CLR_WARNING, "  {d} {s} - drill down with: zfin audit --schwab <account.csv>\n", .{
            discrepancy_count, if (discrepancy_count == 1) @as([]const u8, "mismatch") else @as([]const u8, "mismatches"),
        });
    }
    try out.print("\n", .{});
}

/// Direct-indexing ratio suggestions from Schwab-summary data.
///
/// The Schwab summary path only gives us per-account totals, not
/// per-symbol detail. For a direct-indexing account with exactly one
/// stock lot (the common case - the account is the proxy basket,
/// tracked as a single benchmark lot), we can still emit a ratio
/// suggestion from the account-level `total_delta`:
///
///   current_stock_value = portfolio_total - portfolio_cash
///   target_stock_value  = current_stock_value + total_delta
///   suggested_ratio     = target_stock_value / (shares × price)
///
/// Where `price` is `shares × current_cached_price`. The math
/// assumes the full account delta lands on the single tracked lot,
/// which is the semantics of a direct-indexing proxy.
///
/// Skips accounts with more than one stock lot (can't allocate the
/// delta) or zero stock lots (nothing to adjust).
pub fn displaySchwabSummaryRatioSuggestions(
    allocator: std.mem.Allocator,
    results: []const SchwabAccountComparison,
    portfolio: zfin.Portfolio,
    prices: std.StringHashMap(f64),
    account_map: ?analysis.AccountMap,
    color: bool,
    out: *std.Io.Writer,
) !void {
    const suggestions = try reconcile.summaryRatioSuggestions(allocator, results, portfolio, prices, account_map);
    defer allocator.free(suggestions);
    if (suggestions.len == 0) return;

    try out.print("\n", .{});
    try cli.printBold(out, color, "  Ratio updates", .{});
    try cli.printFg(out, color, cli.CLR_MUTED, "  (for portfolio.srf; direct-indexing accounts)\n", .{});

    for (suggestions) |s| {
        var cur_buf: [24]u8 = undefined;
        var sug_buf: [24]u8 = undefined;
        var drift_buf: [16]u8 = undefined;
        const cur_str = std.fmt.bufPrint(&cur_buf, "{d}", .{s.current_ratio}) catch "?";
        const sug_str = std.fmt.bufPrint(&sug_buf, "{d}", .{s.suggested_ratio}) catch "?";
        const drift_str = std.fmt.bufPrint(&drift_buf, "{d:.4}%", .{s.drift_pct}) catch "?";

        try out.print("    {s:<16} ", .{s.lot_symbol});
        try cli.printFg(out, color, cli.CLR_MUTED, "ticker {s:<6}", .{s.price_symbol});
        try out.print("  ratio {s} -> ", .{cur_str});
        try cli.printBold(out, color, "{s}", .{sug_str});
        try cli.printFg(out, color, cli.CLR_MUTED, "  ({s} drift)\n", .{drift_str});
    }

    try out.print("\n", .{});
}
// ── displaySchwabResults rendering ───────────────────────────

test "displaySchwabResults: renders mapped/cash/value/unmapped rows and totals" {
    const results = [_]SchwabAccountComparison{
        // clean mapped -> no status
        .{ .account_name = "Sample Roth", .schwab_name = "Roth IRA", .account_number = "1234", .portfolio_cash = 100, .schwab_cash = 100, .cash_delta = 0, .portfolio_total = 5000, .schwab_total = 5000, .total_delta = 0, .has_discrepancy = false },
        // cash mismatch -> "Cash +$5.00", counts as a real mismatch
        .{ .account_name = "Sample Trust", .schwab_name = "Trust", .account_number = "5678", .portfolio_cash = 95, .schwab_cash = 100, .cash_delta = 5, .portfolio_total = 8000, .schwab_total = 8005, .total_delta = 5, .has_discrepancy = true },
        // value delta within the CD band (cash ok) -> muted "Value
        // +$100.00", shown but not counted as a real mismatch
        .{ .account_name = "Sample HSA", .schwab_name = "HSA", .account_number = "9012", .portfolio_cash = 50, .schwab_cash = 50, .cash_delta = 0, .portfolio_total = 1000, .schwab_total = 1100, .total_delta = 100, .cd_allowance = 200, .has_discrepancy = false },
        // unmapped, null broker fields -> "Unmapped" + "--", counts as a real mismatch
        .{ .account_name = "", .schwab_name = "Sample Brokerage 3456", .account_number = "3456", .portfolio_cash = 0, .schwab_cash = null, .cash_delta = null, .portfolio_total = 0, .schwab_total = null, .total_delta = null, .has_discrepancy = true },
    };

    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    try displaySchwabResults(&results, false, &w);
    const out = w.buffered();

    try std.testing.expect(std.mem.indexOf(u8, out, "Schwab Account Audit") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Sample Roth") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Cash +") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Value +") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Unmapped") != null);
    // unmapped row falls back to the schwab_name label
    try std.testing.expect(std.mem.indexOf(u8, out, "Sample Brokerage 3456") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "--") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Total: portfolio") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "schwab") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "delta") != null);
    // cash-mismatch + unmapped = 2 real mismatches -> plural
    try std.testing.expect(std.mem.indexOf(u8, out, "mismatches") != null);
}

test "displaySchwabResults: color=true emits ANSI and singular label" {
    const results = [_]SchwabAccountComparison{
        .{ .account_name = "", .schwab_name = "Sample Brokerage 9999", .account_number = "9999", .portfolio_cash = 0, .schwab_cash = null, .cash_delta = null, .portfolio_total = 0, .schwab_total = null, .total_delta = null, .has_discrepancy = true },
    };
    var buf: [2048]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    try displaySchwabResults(&results, true, &w);
    const out = w.buffered();

    try std.testing.expect(std.mem.indexOf(u8, out, "Schwab Account Audit") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") != null);
    // single mismatch -> singular "1 mismatch" label
    try std.testing.expect(std.mem.indexOf(u8, out, "1 mismatch") != null);
}

test "displaySchwabResults: value delta with no CD or options warns and counts" {
    // The Kelly-IRA shape: cash matches to the penny, but the total is
    // off (a missing lot). No CD, no options -> a real, counted warning.
    const results = [_]SchwabAccountComparison{
        .{ .account_name = "Sample IRA", .schwab_name = "IRA", .account_number = "1234", .portfolio_cash = 100, .schwab_cash = 100, .cash_delta = 0, .portfolio_total = 250000, .schwab_total = 251740.29, .total_delta = 1740.29, .has_discrepancy = true },
    };
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    try displaySchwabResults(&results, true, &w);
    const out = w.buffered();

    var seqbuf: [32]u8 = undefined;
    const warn_seq = try std.fmt.bufPrint(&seqbuf, "\x1b[38;2;{d};{d};{d}m", .{ cli.CLR_WARNING[0], cli.CLR_WARNING[1], cli.CLR_WARNING[2] });

    try std.testing.expect(std.mem.indexOf(u8, out, "Value +") != null); // shown
    try std.testing.expect(std.mem.indexOf(u8, out, warn_seq) != null); // warning-colored
    try std.testing.expect(std.mem.indexOf(u8, out, "1 mismatch") != null); // counted
}

test "displaySchwabResults: CD-band and option value deltas are muted, shown, and uncounted" {
    // muted != hidden: both rows show their "Value" delta, but neither
    // is warning-colored and neither counts as a mismatch.
    const results = [_]SchwabAccountComparison{
        // CD account: $8.70 mark sits within a $154 band -> expected/muted.
        .{ .account_name = "Sample IRA", .schwab_name = "IRA", .account_number = "1234", .portfolio_cash = 0, .schwab_cash = 0, .cash_delta = 0, .portfolio_total = 100000, .schwab_total = 99991.30, .total_delta = -8.70, .cd_allowance = 154, .has_discrepancy = true },
        // Option account: cost-vs-market gap is unbounded -> muted, drill down.
        .{ .account_name = "Sample Roth", .schwab_name = "Roth", .account_number = "5678", .portfolio_cash = 0, .schwab_cash = 0, .cash_delta = 0, .portfolio_total = 50000, .schwab_total = 55000, .total_delta = 5000, .has_options = true, .has_discrepancy = true },
    };
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    try displaySchwabResults(&results, true, &w);
    const out = w.buffered();

    var warnbuf: [32]u8 = undefined;
    const warn_seq = try std.fmt.bufPrint(&warnbuf, "\x1b[38;2;{d};{d};{d}m", .{ cli.CLR_WARNING[0], cli.CLR_WARNING[1], cli.CLR_WARNING[2] });
    var mutebuf: [32]u8 = undefined;
    const mute_seq = try std.fmt.bufPrint(&mutebuf, "\x1b[38;2;{d};{d};{d}m", .{ cli.CLR_MUTED[0], cli.CLR_MUTED[1], cli.CLR_MUTED[2] });

    try std.testing.expect(std.mem.indexOf(u8, out, "Value -") != null); // CD mark shown
    try std.testing.expect(std.mem.indexOf(u8, out, "Value +") != null); // option gap shown
    try std.testing.expect(std.mem.indexOf(u8, out, mute_seq) != null); // muted styling present
    try std.testing.expect(std.mem.indexOf(u8, out, warn_seq) == null); // never escalated to warning
    try std.testing.expect(std.mem.indexOf(u8, out, "mismatch") == null); // nothing to investigate
}

// ── displaySchwabSummaryRatioSuggestions ─────────────────────

test "displaySchwabSummaryRatioSuggestions: emits ratio drift for single-lot direct-indexing account" {
    const allocator = std.testing.allocator;
    var lots = [_]portfolio_mod.Lot{
        .{ .symbol = "SPY", .shares = 100, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 400, .account = "Sample Brokerage", .price_ratio = 1.0 },
    };
    const portfolio = portfolio_mod.Portfolio{ .lots = &lots, .allocator = allocator };

    var entries = [_]analysis.AccountTaxEntry{
        .{ .account = "Sample Brokerage", .tax_type = .taxable, .institution = "schwab", .account_number = "1234", .direct_indexing = true },
    };
    const acct_map = analysis.AccountMap{ .entries = &entries, .allocator = allocator };

    var prices = std.StringHashMap(f64).init(allocator);
    defer prices.deinit();
    try prices.put("SPY", 500.0);

    // total_delta 1000 on a 50000 stock value -> suggested ratio 1.02 vs 1.0.
    const results = [_]SchwabAccountComparison{
        .{ .account_name = "Sample Brokerage", .schwab_name = "Brokerage", .account_number = "1234", .portfolio_cash = 0, .schwab_cash = 0, .cash_delta = 0, .portfolio_total = 50000, .schwab_total = 51000, .total_delta = 1000, .has_discrepancy = true },
    };

    var buf: [2048]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    try displaySchwabSummaryRatioSuggestions(allocator, &results, portfolio, prices, acct_map, false, &w);
    const out = w.buffered();

    try std.testing.expect(std.mem.indexOf(u8, out, "Ratio updates") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "SPY") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "ratio 1 -> ") != null);
}

test "displaySchwabSummaryRatioSuggestions: no account_map produces no output" {
    const allocator = std.testing.allocator;
    const portfolio = portfolio_mod.Portfolio{ .lots = &.{}, .allocator = allocator };
    var prices = std.StringHashMap(f64).init(allocator);
    defer prices.deinit();

    const results = [_]SchwabAccountComparison{
        .{ .account_name = "Sample Brokerage", .schwab_name = "Brokerage", .account_number = "1234", .portfolio_cash = 0, .schwab_cash = 0, .cash_delta = 0, .portfolio_total = 50000, .schwab_total = 51000, .total_delta = 1000, .has_discrepancy = true },
    };
    var buf: [512]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    try displaySchwabSummaryRatioSuggestions(allocator, &results, portfolio, prices, null, false, &w);
    try std.testing.expectEqual(@as(usize, 0), w.buffered().len);
}

test "displaySchwabSummaryRatioSuggestions: non-direct-indexing account is skipped" {
    const allocator = std.testing.allocator;
    var lots = [_]portfolio_mod.Lot{
        .{ .symbol = "SPY", .shares = 100, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 400, .account = "Sample Brokerage", .price_ratio = 1.0 },
    };
    const portfolio = portfolio_mod.Portfolio{ .lots = &lots, .allocator = allocator };

    var entries = [_]analysis.AccountTaxEntry{
        .{ .account = "Sample Brokerage", .tax_type = .taxable, .institution = "schwab", .account_number = "1234", .direct_indexing = false },
    };
    const acct_map = analysis.AccountMap{ .entries = &entries, .allocator = allocator };

    var prices = std.StringHashMap(f64).init(allocator);
    defer prices.deinit();
    try prices.put("SPY", 500.0);

    const results = [_]SchwabAccountComparison{
        .{ .account_name = "Sample Brokerage", .schwab_name = "Brokerage", .account_number = "1234", .portfolio_cash = 0, .schwab_cash = 0, .cash_delta = 0, .portfolio_total = 50000, .schwab_total = 51000, .total_delta = 1000, .has_discrepancy = true },
    };
    var buf: [512]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    try displaySchwabSummaryRatioSuggestions(allocator, &results, portfolio, prices, acct_map, false, &w);
    try std.testing.expectEqual(@as(usize, 0), w.buffered().len);
}
