const Date = @import("../Date.zig");
const portfolio = @import("portfolio.zig");

pub const ContractType = enum {
    call,
    put,
};

/// A single options contract in a chain.
pub const OptionContract = struct {
    /// Full OCC symbol (e.g., "O:AAPL211022C000150000")
    contract_symbol: ?[]const u8 = null,
    contract_type: ContractType,
    strike: f64,
    expiration: Date,
    bid: ?f64 = null,
    ask: ?f64 = null,
    last_price: ?f64 = null,
    volume: ?u64 = null,
    open_interest: ?u64 = null,
    implied_volatility: ?f64 = null,
    // Greeks
    delta: ?f64 = null,
    gamma: ?f64 = null,
    theta: ?f64 = null,
    vega: ?f64 = null,
};

/// Full options chain for an underlying asset at a given expiration.
pub const OptionsChain = struct {
    underlying_symbol: []const u8,
    underlying_price: ?f64 = null,
    expiration: Date,
    calls: []const OptionContract,
    puts: []const OptionContract,

    /// Free any owned fields on this chain. Mirrors the pattern in
    /// `Dividend.deinit` - callers who own a single chain can release
    /// it directly; callers with a slice use `freeSlice` below.
    pub fn deinit(self: OptionsChain, allocator: std.mem.Allocator) void {
        allocator.free(self.underlying_symbol);
        allocator.free(self.calls);
        allocator.free(self.puts);
    }

    /// Free a slice of chains, calling `deinit` on each element first.
    pub fn freeSlice(allocator: std.mem.Allocator, chains: []const OptionsChain) void {
        for (chains) |c| c.deinit(allocator);
        allocator.free(chains);
    }
};

const std = @import("std");

/// Match a compact broker-display option symbol against a portfolio
/// lot by parsing the symbol's components (underlying, expiration,
/// call/put, strike) and comparing them to the lot's structured
/// fields.
///
/// Compact format: `[-]{UNDERLYING}{YYMMDD}{C|P}{STRIKE}`
/// (e.g. "-AMZN260515C220"). This is the form brokers like Fidelity
/// and Schwab use in their positions exports - distinct from the
/// canonical 21-char OCC symbol with a zero-padded strike. The
/// underlying length is variable, so we scan for the first position
/// where 6 consecutive digits encode a valid date.
///
/// Lives here (the option model) rather than in any one broker's
/// parser because the audit reconciler applies it across brokers -
/// keeping it broker-neutral lets the shared comparison engine in
/// `commands/audit/common.zig` stay decoupled from `brokerage/*`.
pub fn symbolMatchesLot(symbol: []const u8, lot: portfolio.Lot) bool {
    if (lot.security_type != .option) return false;

    // Strip leading dash (short indicator)
    const sym = if (symbol.len > 0 and symbol[0] == '-') symbol[1..] else symbol;

    // Need at least: 1 char underlying + 6 date + 1 type + 1 strike = 9
    if (sym.len < 9) return false;

    // Scan for the date boundary: first position where 6 consecutive digits
    // form a valid YYMMDD (and the character before is a letter).
    var i: usize = 1; // underlying is at least 1 char
    while (i + 7 < sym.len) : (i += 1) {
        // All 6 chars must be digits
        if (!std.ascii.isDigit(sym[i]) or
            !std.ascii.isDigit(sym[i + 1]) or
            !std.ascii.isDigit(sym[i + 2]) or
            !std.ascii.isDigit(sym[i + 3]) or
            !std.ascii.isDigit(sym[i + 4]) or
            !std.ascii.isDigit(sym[i + 5]))
            continue;

        // Character after the 6 digits must be C or P
        const type_char = sym[i + 6];
        if (type_char != 'C' and type_char != 'P') continue;

        // Parse date components
        const yy = std.fmt.parseInt(i16, sym[i..][0..2], 10) catch continue;
        const mm = std.fmt.parseInt(u8, sym[i + 2 ..][0..2], 10) catch continue;
        const dd = std.fmt.parseInt(u8, sym[i + 4 ..][0..2], 10) catch continue;
        if (mm < 1 or mm > 12 or dd < 1 or dd > 31) continue;
        const year = 2000 + yy;

        // Parse components
        const underlying = sym[0..i];
        const option_type: portfolio.OptionType = if (type_char == 'P') .put else .call;
        const strike_str = sym[i + 7 ..];
        const strike = std.fmt.parseFloat(f64, strike_str) catch continue;
        const date = Date.fromYmd(year, mm, dd);

        // Match against lot fields
        const lot_underlying = lot.underlying orelse return false;
        const lot_maturity = lot.maturity_date orelse return false;

        if (!std.mem.eql(u8, underlying, lot_underlying)) return false;
        if (!lot_maturity.eql(date)) return false;
        if (option_type != lot.option_type) return false;
        if (lot.strike) |ls| {
            if (@abs(ls - strike) > 0.01) return false;
        } else return false;

        return true;
    }
    return false;
}

test "symbolMatchesLot basic call" {
    const lot = portfolio.Lot{
        .symbol = "AMZN 05/15/2026 220.00 C",
        .security_type = .option,
        .underlying = "AMZN",
        .strike = 220.0,
        .option_type = .call,
        .maturity_date = Date.fromYmd(2026, 5, 15),
        .shares = -3,
        .open_date = Date.fromYmd(2025, 1, 1),
        .open_price = 8.75,
    };

    // Compact format with leading dash (short)
    try std.testing.expect(symbolMatchesLot("-AMZN260515C220", lot));
    // Without dash
    try std.testing.expect(symbolMatchesLot("AMZN260515C220", lot));
    // Wrong underlying
    try std.testing.expect(!symbolMatchesLot("-MSFT260515C220", lot));
    // Wrong date
    try std.testing.expect(!symbolMatchesLot("-AMZN260615C220", lot));
    // Wrong type
    try std.testing.expect(!symbolMatchesLot("-AMZN260515P220", lot));
    // Wrong strike
    try std.testing.expect(!symbolMatchesLot("-AMZN260515C230", lot));
    // Non-option lot
    const stock_lot = portfolio.Lot{ .symbol = "AMZN", .security_type = .stock, .shares = 100, .open_date = Date.fromYmd(2025, 1, 1), .open_price = 100 };
    try std.testing.expect(!symbolMatchesLot("-AMZN260515C220", stock_lot));
}

test "symbolMatchesLot put option and decimal strike" {
    const lot = portfolio.Lot{
        .symbol = "AAPL 06/20/2026 220.50 P",
        .security_type = .option,
        .underlying = "AAPL",
        .strike = 220.50,
        .option_type = .put,
        .maturity_date = Date.fromYmd(2026, 6, 20),
        .shares = -1,
        .open_date = Date.fromYmd(2025, 1, 1),
        .open_price = 5.0,
    };

    try std.testing.expect(symbolMatchesLot("-AAPL260620P220.50", lot));
    try std.testing.expect(symbolMatchesLot("AAPL260620P220.50", lot));
    // Call doesn't match put
    try std.testing.expect(!symbolMatchesLot("-AAPL260620C220.50", lot));
}

test "symbolMatchesLot single-char underlying" {
    const lot = portfolio.Lot{
        .symbol = "A 03/20/2026 150.00 C",
        .security_type = .option,
        .underlying = "A",
        .strike = 150.0,
        .option_type = .call,
        .maturity_date = Date.fromYmd(2026, 3, 20),
        .shares = -2,
        .open_date = Date.fromYmd(2025, 1, 1),
        .open_price = 3.0,
    };

    try std.testing.expect(symbolMatchesLot("-A260320C150", lot));
    try std.testing.expect(!symbolMatchesLot("-A260320P150", lot));
}

test "symbolMatchesLot: option lot with null strike never matches" {
    // Everything else lines up (underlying/date/type), but a lot with
    // no strike can't be a strike match -> the `else return false` arm.
    const lot = portfolio.Lot{
        .symbol = "AAPL 06/20/2026 C",
        .security_type = .option,
        .underlying = "AAPL",
        .strike = null,
        .option_type = .call,
        .maturity_date = Date.fromYmd(2026, 6, 20),
        .shares = -1,
        .open_date = Date.fromYmd(2025, 1, 1),
        .open_price = 5.0,
    };
    try std.testing.expect(!symbolMatchesLot("-AAPL260620C220", lot));
}

test "symbolMatchesLot: symbol with no valid date boundary falls through to false" {
    // Long enough to enter the scan loop, but no 6-digit run followed
    // by C/P -> the loop exhausts and returns false.
    const lot = portfolio.Lot{
        .symbol = "AAPL 06/20/2026 220.00 C",
        .security_type = .option,
        .underlying = "AAPL",
        .strike = 220.0,
        .option_type = .call,
        .maturity_date = Date.fromYmd(2026, 6, 20),
        .shares = -1,
        .open_date = Date.fromYmd(2025, 1, 1),
        .open_price = 5.0,
    };
    try std.testing.expect(!symbolMatchesLot("NODATEHERE", lot));
}

test "OptionsChain.deinit and freeSlice free owned memory" {
    const allocator = std.testing.allocator;

    // Single-chain deinit.
    {
        const chain = OptionsChain{
            .underlying_symbol = try allocator.dupe(u8, "AAPL"),
            .expiration = Date.fromYmd(2026, 6, 20),
            .calls = try allocator.alloc(OptionContract, 0),
            .puts = try allocator.alloc(OptionContract, 0),
        };
        chain.deinit(allocator);
    }

    // Slice freeSlice (deinits each element first).
    {
        const chains = try allocator.alloc(OptionsChain, 1);
        chains[0] = .{
            .underlying_symbol = try allocator.dupe(u8, "MSFT"),
            .expiration = Date.fromYmd(2026, 6, 20),
            .calls = try allocator.alloc(OptionContract, 0),
            .puts = try allocator.alloc(OptionContract, 0),
        };
        OptionsChain.freeSlice(allocator, chains);
    }
}
