//! Chart-to-PNG export.
//!
//! The CLI's `--export-chart <path>` flag funnels through this
//! module. Each chart-bearing command (`quote`, `projections`)
//! has a corresponding `export*` function here that:
//!
//!   1. Calls the relevant `renderToSurface` (in `tui/chart.zig` or
//!      `tui/projection_chart.zig`) to draw the chart into a z2d
//!      `Surface`.
//!   2. Calls `z2d.png_exporter.writeToPNGFile` to land the surface
//!      as a PNG file at the user-supplied path.
//!   3. Frees the surface.
//!
//! Default export resolution is 1920x1080 — matches the TUI's
//! `chart_config.max_width`/`max_height` defaults so the exported
//! image has the same fidelity the user sees in the terminal.
//! The TUI's adaptive chart sizing isn't used here because the
//! export target is a file, not a cell grid.
//!
//! The module deliberately reuses the TUI's `default_theme` for
//! consistent visual identity across the live TUI surface and
//! exported images. A `--theme` override at export time is out of
//! scope for V1 (see TODO follow-up).

const std = @import("std");
const z2d = @import("z2d");
const zfin = @import("root.zig");
const chart = @import("tui/chart.zig");
const projection_chart = @import("tui/projection_chart.zig");
const projections = @import("analytics/projections.zig");
const theme = @import("tui/theme.zig");

/// Default PNG export resolution. Matches `tui/chart.zig`'s
/// `ChartConfig.max_width/max_height` defaults so an exported
/// image carries the same fidelity as a maximally-sized TUI
/// chart.
pub const default_width: u32 = 1920;
pub const default_height: u32 = 1080;

/// Export a price+Bollinger+RSI chart for a single symbol.
/// Wraps `chart.renderToSurface` + `writeToPNGFile`.
pub fn exportSymbolChart(
    io: std.Io,
    alloc: std.mem.Allocator,
    candles: []const zfin.Candle,
    timeframe: chart.Timeframe,
    path: []const u8,
) !void {
    var rendered = chart.renderToSurface(
        io,
        alloc,
        candles,
        timeframe,
        default_width,
        default_height,
        theme.default_theme,
        null,
    ) catch |err| switch (err) {
        error.InsufficientData => return error.InsufficientData,
        else => return err,
    };
    defer rendered.deinit(alloc);

    try z2d.png_exporter.writeToPNGFile(io, rendered.surface, path, .{});
}

/// Export a projection percentile-band chart with optional actuals
/// overlay. Wraps `projection_chart.renderToSurface` +
/// `writeToPNGFile`.
pub fn exportProjectionChart(
    io: std.Io,
    alloc: std.mem.Allocator,
    bands: []const projections.YearPercentiles,
    actuals: ?projection_chart.ActualsOverlay,
    path: []const u8,
) !void {
    var rendered = projection_chart.renderToSurface(
        io,
        alloc,
        bands,
        default_width,
        default_height,
        theme.default_theme,
        actuals,
    ) catch |err| switch (err) {
        error.InsufficientData => return error.InsufficientData,
        else => return err,
    };
    defer rendered.deinit(alloc);

    try z2d.png_exporter.writeToPNGFile(io, rendered.surface, path, .{});
}

// ── Tests ─────────────────────────────────────────────────────────────

test "exportSymbolChart writes a non-empty PNG file" {
    const Date = @import("Date.zig");
    const alloc = std.testing.allocator;
    const io = std.testing.io;

    // Minimum candle count (renderToSurface requires >= 20)
    var candles: [25]zfin.Candle = undefined;
    for (0..25) |i| {
        const price: f64 = 100.0 + @as(f64, @floatFromInt(i));
        candles[i] = .{
            .date = Date.fromYmd(2024, 1, 2).addDays(@intCast(i)),
            .open = price,
            .high = price + 1,
            .low = price - 1,
            .close = price,
            .adj_close = price,
            .volume = 1000,
        };
    }

    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var path_buf: [std.fs.max_path_bytes]u8 = undefined;
    const dir_len = try tmp.dir.realPathFile(io, ".", &path_buf);
    const dir_path = path_buf[0..dir_len];
    const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_symbol.png" });
    defer alloc.free(path);

    try exportSymbolChart(io, alloc, &candles, .@"6M", path);

    // Verify the file exists, starts with the PNG magic, and is
    // big enough to plausibly contain a chart (not just headers).
    var file = try tmp.dir.openFile(io, "test_export_symbol.png", .{});
    defer file.close(io);
    const size = (try file.stat(io)).size;
    try std.testing.expect(size > 1024); // arbitrary lower bound

    var magic: [8]u8 = undefined;
    var reader = file.reader(io, &.{});
    _ = try reader.interface.readSliceShort(&magic);
    try std.testing.expectEqualSlices(u8, "\x89PNG\x0D\x0A\x1A\x0A", &magic);
}

test "exportSymbolChart returns InsufficientData on too-few candles" {
    const Date = @import("Date.zig");
    const alloc = std.testing.allocator;
    const io = std.testing.io;

    var candles: [10]zfin.Candle = undefined;
    for (0..10) |i| {
        candles[i] = .{
            .date = Date.fromYmd(2024, 1, 2).addDays(@intCast(i)),
            .open = 100,
            .high = 100,
            .low = 100,
            .close = 100,
            .adj_close = 100,
            .volume = 0,
        };
    }

    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var path_buf: [std.fs.max_path_bytes]u8 = undefined;
    const dir_len = try tmp.dir.realPathFile(io, ".", &path_buf);
    const dir_path = path_buf[0..dir_len];
    const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_symbol_insufficient.png" });
    defer alloc.free(path);

    try std.testing.expectError(
        error.InsufficientData,
        exportSymbolChart(io, alloc, &candles, .@"6M", path),
    );
}

test "exportProjectionChart writes a non-empty PNG file" {
    const alloc = std.testing.allocator;
    const io = std.testing.io;

    // Synthetic 11-year horizon: bands grow linearly, all
    // percentiles spread around p50.
    var bands: [11]projections.YearPercentiles = undefined;
    for (0..11) |i| {
        const base: f64 = 100_000.0 * (1.0 + 0.07 * @as(f64, @floatFromInt(i)));
        bands[i] = .{
            .year = @intCast(i),
            .p10 = base * 0.6,
            .p25 = base * 0.8,
            .p50 = base,
            .p75 = base * 1.2,
            .p90 = base * 1.5,
        };
    }

    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var path_buf: [std.fs.max_path_bytes]u8 = undefined;
    const dir_len = try tmp.dir.realPathFile(io, ".", &path_buf);
    const dir_path = path_buf[0..dir_len];
    const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_projection.png" });
    defer alloc.free(path);

    try exportProjectionChart(io, alloc, &bands, null, path);

    var file = try tmp.dir.openFile(io, "test_export_projection.png", .{});
    defer file.close(io);
    const size = (try file.stat(io)).size;
    try std.testing.expect(size > 1024);

    var magic: [8]u8 = undefined;
    var reader = file.reader(io, &.{});
    _ = try reader.interface.readSliceShort(&magic);
    try std.testing.expectEqualSlices(u8, "\x89PNG\x0D\x0A\x1A\x0A", &magic);
}

test "exportProjectionChart returns InsufficientData with single band" {
    const alloc = std.testing.allocator;
    const io = std.testing.io;

    var bands: [1]projections.YearPercentiles = .{.{
        .year = 0,
        .p10 = 100,
        .p25 = 110,
        .p50 = 120,
        .p75 = 130,
        .p90 = 140,
    }};

    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    var path_buf: [std.fs.max_path_bytes]u8 = undefined;
    const dir_len = try tmp.dir.realPathFile(io, ".", &path_buf);
    const dir_path = path_buf[0..dir_len];
    const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_projection_insufficient.png" });
    defer alloc.free(path);

    try std.testing.expectError(
        error.InsufficientData,
        exportProjectionChart(io, alloc, &bands, null, path),
    );
}
