use std::collections::HashMap;

use thiserror::Error;

use crate::portfolio::{
    OpenLot, PortfolioLedger, PortfolioTransaction, PositionSnapshot, ReplaySnapshot,
    TransactionKind,
};

const FLOAT_TOLERANCE: f64 = 1e-9;

#[derive(Debug, Error, Clone, PartialEq)]
pub enum PortfolioEngineError {
    #[error("ledger transaction {transaction_id} has invalid numeric fields")]
    InvalidTransactionNumbers { transaction_id: String },
    #[error("ledger transaction {transaction_id} is missing required fields")]
    MissingTransactionFields { transaction_id: String },
    #[error("ledger transaction {transaction_id} would make cash negative by {shortfall:.2}")]
    NegativeCash {
        transaction_id: String,
        shortfall: f64,
    },
    #[error(
        "ledger transaction {transaction_id} tries to sell {requested_quantity:.4} shares of {symbol} but only {available_quantity:.4} are available"
    )]
    InsufficientShares {
        transaction_id: String,
        symbol: String,
        requested_quantity: f64,
        available_quantity: f64,
    },
}

pub fn replay_ledger(ledger: &PortfolioLedger) -> Result<ReplaySnapshot, PortfolioEngineError> {
    let mut ordered_transactions = ledger.transactions.clone();
    ordered_transactions.sort_by(|left, right| {
        left.executed_at
            .cmp(&right.executed_at)
            .then_with(|| left.id.cmp(&right.id))
    });

    let mut cash_balance = 0.0;
    let mut realized_gain = 0.0;
    let mut lots_by_symbol: HashMap<String, Vec<OpenLot>> = HashMap::new();
    let mut names_by_symbol: HashMap<String, String> = HashMap::new();
    let mut latest_trade_at: HashMap<String, String> = HashMap::new();
    let mut latest_trade_price: HashMap<String, f64> = HashMap::new();

    for transaction in &ordered_transactions {
        match transaction.kind {
            TransactionKind::CashDeposit => {
                validate_cash_amount(transaction)?;
                cash_balance += transaction.gross_amount;
            }
            TransactionKind::CashWithdrawal => {
                validate_cash_amount(transaction)?;
                ensure_cash_available(cash_balance, transaction.gross_amount, &transaction.id)?;
                cash_balance -= transaction.gross_amount;
            }
            TransactionKind::Buy => {
                let (symbol, company_name, quantity, price) =
                    validate_equity_transaction(transaction)?;
                let trade_total = quantity * price + transaction.fee;
                ensure_cash_available(cash_balance, trade_total, &transaction.id)?;
                cash_balance -= trade_total;
                lots_by_symbol
                    .entry(symbol.clone())
                    .or_default()
                    .push(OpenLot { quantity, price });
                names_by_symbol.insert(symbol.clone(), company_name);
                latest_trade_at.insert(symbol.clone(), transaction.executed_at.clone());
                latest_trade_price.insert(symbol, price);
            }
            TransactionKind::Sell => {
                let (symbol, company_name, quantity, price) =
                    validate_equity_transaction(transaction)?;
                let Some(open_lots) = lots_by_symbol.get_mut(&symbol) else {
                    return Err(PortfolioEngineError::InsufficientShares {
                        transaction_id: transaction.id.clone(),
                        symbol,
                        requested_quantity: quantity,
                        available_quantity: 0.0,
                    });
                };

                let available_quantity = open_lots.iter().map(|lot| lot.quantity).sum::<f64>();
                if available_quantity + FLOAT_TOLERANCE < quantity {
                    return Err(PortfolioEngineError::InsufficientShares {
                        transaction_id: transaction.id.clone(),
                        symbol,
                        requested_quantity: quantity,
                        available_quantity,
                    });
                }

                let depleted_cost = consume_fifo_lots(open_lots, quantity);
                open_lots.retain(|lot| lot.quantity > FLOAT_TOLERANCE);

                if open_lots.is_empty() {
                    lots_by_symbol.remove(&symbol);
                }

                let proceeds = quantity * price - transaction.fee;
                realized_gain += proceeds - depleted_cost;
                cash_balance += proceeds;
                names_by_symbol.insert(symbol.clone(), company_name);
                latest_trade_at.insert(symbol.clone(), transaction.executed_at.clone());
                latest_trade_price.insert(symbol, price);
            }
        }
    }

    let mut positions = lots_by_symbol
        .into_iter()
        .filter_map(|(symbol, open_lots)| {
            let quantity = open_lots.iter().map(|lot| lot.quantity).sum::<f64>();
            if quantity <= FLOAT_TOLERANCE {
                return None;
            }

            Some(PositionSnapshot {
                cost_basis: open_lots
                    .iter()
                    .map(|lot| lot.quantity * lot.price)
                    .sum::<f64>(),
                company_name: names_by_symbol
                    .get(&symbol)
                    .cloned()
                    .unwrap_or_else(|| symbol.clone()),
                latest_trade_at: latest_trade_at.get(&symbol).cloned(),
                latest_trade_price: latest_trade_price.get(&symbol).copied(),
                open_lots,
                quantity,
                symbol,
            })
        })
        .collect::<Vec<_>>();
    positions.sort_by(|left, right| left.symbol.cmp(&right.symbol));

    Ok(ReplaySnapshot {
        cash_balance,
        realized_gain,
        positions,
    })
}

fn validate_cash_amount(transaction: &PortfolioTransaction) -> Result<(), PortfolioEngineError> {
    if !transaction.gross_amount.is_finite()
        || !transaction.fee.is_finite()
        || transaction.gross_amount <= 0.0
        || transaction.fee < 0.0
    {
        return Err(PortfolioEngineError::InvalidTransactionNumbers {
            transaction_id: transaction.id.clone(),
        });
    }

    Ok(())
}

fn validate_equity_transaction(
    transaction: &PortfolioTransaction,
) -> Result<(String, String, f64, f64), PortfolioEngineError> {
    let Some(symbol) = transaction
        .symbol
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    else {
        return Err(PortfolioEngineError::MissingTransactionFields {
            transaction_id: transaction.id.clone(),
        });
    };

    let Some(company_name) = transaction
        .company_name
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    else {
        return Err(PortfolioEngineError::MissingTransactionFields {
            transaction_id: transaction.id.clone(),
        });
    };

    let Some(quantity) = transaction.quantity else {
        return Err(PortfolioEngineError::MissingTransactionFields {
            transaction_id: transaction.id.clone(),
        });
    };
    let Some(price) = transaction.price else {
        return Err(PortfolioEngineError::MissingTransactionFields {
            transaction_id: transaction.id.clone(),
        });
    };

    if !quantity.is_finite()
        || !price.is_finite()
        || !transaction.gross_amount.is_finite()
        || !transaction.fee.is_finite()
        || quantity <= 0.0
        || price <= 0.0
        || transaction.gross_amount <= 0.0
        || transaction.fee < 0.0
    {
        return Err(PortfolioEngineError::InvalidTransactionNumbers {
            transaction_id: transaction.id.clone(),
        });
    }

    Ok((
        symbol.to_string(),
        company_name.to_string(),
        quantity,
        price,
    ))
}

fn ensure_cash_available(
    cash_balance: f64,
    required_amount: f64,
    transaction_id: &str,
) -> Result<(), PortfolioEngineError> {
    if cash_balance + FLOAT_TOLERANCE < required_amount {
        return Err(PortfolioEngineError::NegativeCash {
            transaction_id: transaction_id.to_string(),
            shortfall: required_amount - cash_balance,
        });
    }

    Ok(())
}

fn consume_fifo_lots(open_lots: &mut [OpenLot], requested_quantity: f64) -> f64 {
    let mut remaining_quantity = requested_quantity;
    let mut depleted_cost = 0.0;

    for lot in open_lots {
        if remaining_quantity <= FLOAT_TOLERANCE {
            break;
        }

        let consumed_quantity = lot.quantity.min(remaining_quantity);
        depleted_cost += consumed_quantity * lot.price;
        lot.quantity -= consumed_quantity;
        remaining_quantity -= consumed_quantity;
    }

    depleted_cost
}

#[cfg(test)]
mod tests {
    use super::replay_ledger;
    use crate::portfolio::{PortfolioLedger, PortfolioTransaction, TransactionKind};

    fn buy(
        id: &str,
        symbol: &str,
        quantity: f64,
        price: f64,
        executed_at: &str,
    ) -> PortfolioTransaction {
        PortfolioTransaction {
            id: id.to_string(),
            kind: TransactionKind::Buy,
            symbol: Some(symbol.to_string()),
            company_name: Some(format!("{symbol} Corp")),
            quantity: Some(quantity),
            price: Some(price),
            gross_amount: quantity * price,
            fee: 0.0,
            executed_at: executed_at.to_string(),
            note: None,
        }
    }

    fn sell(
        id: &str,
        symbol: &str,
        quantity: f64,
        price: f64,
        executed_at: &str,
    ) -> PortfolioTransaction {
        PortfolioTransaction {
            id: id.to_string(),
            kind: TransactionKind::Sell,
            symbol: Some(symbol.to_string()),
            company_name: Some(format!("{symbol} Corp")),
            quantity: Some(quantity),
            price: Some(price),
            gross_amount: quantity * price,
            fee: 0.0,
            executed_at: executed_at.to_string(),
            note: None,
        }
    }

    fn cash(
        id: &str,
        kind: TransactionKind,
        amount: f64,
        executed_at: &str,
    ) -> PortfolioTransaction {
        PortfolioTransaction {
            id: id.to_string(),
            kind,
            symbol: None,
            company_name: None,
            quantity: None,
            price: None,
            gross_amount: amount,
            fee: 0.0,
            executed_at: executed_at.to_string(),
            note: None,
        }
    }

    #[test]
    fn replay_creates_open_lot_and_reduces_cash_for_buy() {
        let ledger = PortfolioLedger {
            transactions: vec![
                cash(
                    "1",
                    TransactionKind::CashDeposit,
                    1_000.0,
                    "2026-01-01T09:00:00Z",
                ),
                buy("2", "AAPL", 5.0, 100.0, "2026-01-01T10:00:00Z"),
            ],
            ..PortfolioLedger::default()
        };

        let snapshot = replay_ledger(&ledger).expect("replay should succeed");

        assert_eq!(snapshot.cash_balance, 500.0);
        assert_eq!(snapshot.positions.len(), 1);
        assert_eq!(snapshot.positions[0].quantity, 5.0);
    }

    #[test]
    fn replay_keeps_multiple_fifo_lots_for_multiple_buys() {
        let ledger = PortfolioLedger {
            transactions: vec![
                cash(
                    "1",
                    TransactionKind::CashDeposit,
                    2_000.0,
                    "2026-01-01T09:00:00Z",
                ),
                buy("2", "AAPL", 5.0, 100.0, "2026-01-01T10:00:00Z"),
                buy("3", "AAPL", 2.0, 120.0, "2026-01-02T10:00:00Z"),
            ],
            ..PortfolioLedger::default()
        };

        let snapshot = replay_ledger(&ledger).expect("replay should succeed");

        assert_eq!(snapshot.positions[0].open_lots.len(), 2);
        assert_eq!(snapshot.positions[0].quantity, 7.0);
    }

    #[test]
    fn replay_consumes_earliest_lots_first_for_sell() {
        let ledger = PortfolioLedger {
            transactions: vec![
                cash(
                    "1",
                    TransactionKind::CashDeposit,
                    3_000.0,
                    "2026-01-01T09:00:00Z",
                ),
                buy("2", "AAPL", 5.0, 100.0, "2026-01-01T10:00:00Z"),
                buy("3", "AAPL", 5.0, 120.0, "2026-01-02T10:00:00Z"),
                sell("4", "AAPL", 6.0, 130.0, "2026-01-03T10:00:00Z"),
            ],
            ..PortfolioLedger::default()
        };

        let snapshot = replay_ledger(&ledger).expect("replay should succeed");

        assert_eq!(snapshot.positions[0].quantity, 4.0);
        assert_eq!(snapshot.positions[0].open_lots[0].quantity, 4.0);
        assert_eq!(snapshot.positions[0].open_lots[0].price, 120.0);
    }

    #[test]
    fn replay_computes_realized_gain_across_multiple_lots() {
        let ledger = PortfolioLedger {
            transactions: vec![
                cash(
                    "1",
                    TransactionKind::CashDeposit,
                    3_000.0,
                    "2026-01-01T09:00:00Z",
                ),
                buy("2", "AAPL", 5.0, 100.0, "2026-01-01T10:00:00Z"),
                buy("3", "AAPL", 5.0, 120.0, "2026-01-02T10:00:00Z"),
                sell("4", "AAPL", 6.0, 130.0, "2026-01-03T10:00:00Z"),
            ],
            ..PortfolioLedger::default()
        };

        let snapshot = replay_ledger(&ledger).expect("replay should succeed");

        assert_eq!(snapshot.realized_gain, 160.0);
    }

    #[test]
    fn replay_updates_cash_for_cash_deposit_and_withdrawal() {
        let ledger = PortfolioLedger {
            transactions: vec![
                cash(
                    "1",
                    TransactionKind::CashDeposit,
                    1_500.0,
                    "2026-01-01T09:00:00Z",
                ),
                cash(
                    "2",
                    TransactionKind::CashWithdrawal,
                    250.0,
                    "2026-01-01T10:00:00Z",
                ),
            ],
            ..PortfolioLedger::default()
        };

        let snapshot = replay_ledger(&ledger).expect("replay should succeed");

        assert_eq!(snapshot.cash_balance, 1_250.0);
    }

    #[test]
    fn replay_returns_zeroed_snapshot_for_empty_ledger() {
        let snapshot = replay_ledger(&PortfolioLedger::default()).expect("replay should succeed");

        assert_eq!(snapshot.cash_balance, 0.0);
        assert_eq!(snapshot.realized_gain, 0.0);
        assert!(snapshot.positions.is_empty());
    }
}
