//! Hedge calculation for risk-free (or risk-tolerant) cross-matching.
//!
//! This module implements the algorithm to find hedge leg stakes that satisfy
//! the risk constraints for a 3-runner market, for both BACK and LAY user orders.

use crate::book::protocol::command::Side;
use crate::types::{Money, OddsX10000, RunnerId};

fn clamp_i128_to_i64(v: i128) -> i64 {
    if v > i64::MAX as i128 {
        i64::MAX
    } else if v < i64::MIN as i128 {
        i64::MIN
    } else {
        v as i64
    }
}

fn profit_quanta(stake: Money, odds: OddsX10000) -> i64 {
    // Keep this consistent with the engine's settlement/PnL math:
    // profit = stake * (odds - 1) = stake * (odds_x10000 - 10_000) / 10_000
    let stake_i = stake.0 as i128;
    let odds_i = odds.0 as i128;
    let profit = (stake_i * (odds_i - 10_000)) / 10_000;
    clamp_i128_to_i64(profit)
}

// Hard cap search density so runtime does not scale linearly with raw depth quanta.
const MAX_HEDGE_SCAN_POINTS: i64 = 200_000;

fn scan_stride(max_inclusive: i64) -> i64 {
    if max_inclusive <= 0 {
        return 1;
    }
    let span = max_inclusive.saturating_add(1);
    if span <= MAX_HEDGE_SCAN_POINTS {
        1
    } else {
        (span + MAX_HEDGE_SCAN_POINTS - 1) / MAX_HEDGE_SCAN_POINTS
    }
}

fn scan_range_with_stride(start: i64, end: i64, stride: i64, mut f: impl FnMut(i64)) {
    if start > end {
        return;
    }
    let step = stride.max(1);
    let mut x = start;
    let mut last = start.saturating_sub(1);
    while x <= end {
        f(x);
        last = x;
        let next = x.saturating_add(step);
        if next <= x {
            break;
        }
        x = next;
    }
    if last != end {
        f(end);
    }
}

/// A calculated hedge leg for cross-matching.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HedgeLeg {
    /// Runner to hedge on
    pub runner_id: RunnerId,
    /// Odds to place the hedge at
    pub odds: OddsX10000,
    /// Stake for this hedge leg
    pub stake: Money,
    /// Side of the hedge order (opposite of user's side on target runner)
    pub side: Side,
}

/// Result of hedge calculation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HedgeResult {
    /// Successfully found hedge legs that satisfy constraints.
    Success {
        /// Hedge legs to execute
        legs: Vec<HedgeLeg>,
        /// Worst-case P&L across all outcomes (should be >= -tolerance)
        worst_case_pnl: Money,
    },
    /// No valid hedge exists within risk tolerance.
    NoSolution { reason: &'static str },
}

/// Input for hedge calculation.
#[derive(Debug, Clone)]
pub struct HedgeInput {
    /// Runner the user is betting on
    pub target_runner: RunnerId,
    /// User's side (Back or Lay)
    pub user_side: Side,
    /// User's odds
    pub user_odds: OddsX10000,
    /// User's stake
    pub user_stake: Money,
    /// Other runners with their best prices and available liquidity.
    /// For BACK user orders: best BACK prices (what house can LAY against)
    /// For LAY user orders: best LAY prices (what house can BACK against)
    pub other_runners: Vec<(RunnerId, OddsX10000, Money)>, // (runner, odds, available_size)
    /// Maximum allowed loss (from RiskTolerance.effective_tolerance)
    pub max_loss: Money,
}

/// Calculate hedge legs for a 3-runner cross-match.
///
/// Dispatches to the appropriate algorithm based on user's side.
pub fn calculate_3runner_hedge(input: &HedgeInput) -> HedgeResult {
    // Validate we have exactly 2 other runners
    if input.other_runners.len() != 2 {
        return HedgeResult::NoSolution {
            reason: "Expected exactly 2 other runners for 3-runner market",
        };
    }

    match input.user_side {
        Side::Yes => calculate_back_hedge(input),
        Side::No => calculate_lay_hedge(input),
    }
}

/// Calculate hedge for a user BACK order.
///
/// User backs runner A at odds `a` for stake `s`.
/// House LAYs A (matches user), then LAYs other runners as hedge.
///
/// P&L:
/// - P_A = -L + tB + tC  (house loses liability, wins hedge stakes)
/// - P_B = s + tC - tB*(oB-1)  (house wins user stake, wins tC, pays out on B)
/// - P_C = s + tB - tC*(oC-1)  (house wins user stake, wins tB, pays out on C)
///
/// Constraints:
/// - tB + tC >= L - slack  (cover liability minus allowed loss if A wins)
/// - tB <= (s + tC + slack) / (oB - 1)  (limit loss if B wins)
/// - tC <= (s + tB + slack) / (oC - 1)  (limit loss if C wins)
fn calculate_back_hedge(input: &HedgeInput) -> HedgeResult {
    let (runner_b, odds_b, size_b) = input.other_runners[0];
    let (runner_c, odds_c, size_c) = input.other_runners[1];
    if odds_b.0 <= 10_000 || odds_c.0 <= 10_000 {
        return HedgeResult::NoSolution {
            reason: "Invalid odds (must be > 1.0)",
        };
    }

    let s = input.user_stake.0;
    let slack = input.max_loss.0.max(0);
    let size_b = size_b.0.max(0);
    let size_c = size_c.0.max(0);
    let liability = profit_quanta(input.user_stake, input.user_odds).max(0);
    let min_hedge_required = liability.saturating_sub(slack);

    if size_b.saturating_add(size_c) < min_hedge_required {
        return HedgeResult::NoSolution {
            reason: "Insufficient liquidity",
        };
    }

    let mut best: Option<(i64, i64, i64)> = None; // (tb, tc, worst_pnl)
    let scan_b_end = size_b.min(
        max_stake_for_profit_limit(s.saturating_add(size_c).saturating_add(slack), odds_b).max(0),
    );
    let scan_c_end = size_c.min(
        max_stake_for_profit_limit(s.saturating_add(size_b).saturating_add(slack), odds_c).max(0),
    );
    let scan_tb = scan_b_end <= scan_c_end;

    if scan_tb {
        let eval_tb = |tb: i64, best: &mut Option<(i64, i64, i64)>| {
            let lower_a = min_hedge_required.saturating_sub(tb);
            let lower_b = profit_quanta(Money(tb), odds_b).saturating_sub(s.saturating_add(slack));
            let tc_min = lower_a.max(lower_b).max(0);
            let tc_max = size_c.min(max_stake_for_profit_limit(
                s.saturating_add(tb).saturating_add(slack),
                odds_c,
            ));
            if tc_min > tc_max {
                return;
            }
            let tc = tc_min;
            let worst_pnl = back_worst_pnl(liability, s, odds_b, odds_c, tb, tc);
            if worst_pnl < -slack {
                return;
            }
            choose_better_candidate(best, tb, tc, worst_pnl);
        };

        let stride = scan_stride(scan_b_end);
        scan_range_with_stride(0, scan_b_end, stride, |tb| eval_tb(tb, &mut best));
        if stride > 1 {
            let offset = stride / 2;
            if offset > 0 {
                scan_range_with_stride(offset.min(scan_b_end), scan_b_end, stride, |tb| {
                    eval_tb(tb, &mut best)
                });
            }
            let center = best
                .map(|(tb, _, _)| tb)
                .unwrap_or(min_hedge_required.clamp(0, scan_b_end));
            let lo = center.saturating_sub(stride).max(0);
            let hi = center.saturating_add(stride).min(scan_b_end);
            scan_range_with_stride(lo, hi, 1, |tb| eval_tb(tb, &mut best));
        }
    } else {
        let eval_tc = |tc: i64, best: &mut Option<(i64, i64, i64)>| {
            let lower_a = min_hedge_required.saturating_sub(tc);
            let lower_c = profit_quanta(Money(tc), odds_c).saturating_sub(s.saturating_add(slack));
            let tb_min = lower_a.max(lower_c).max(0);
            let tb_max = size_b.min(max_stake_for_profit_limit(
                s.saturating_add(tc).saturating_add(slack),
                odds_b,
            ));
            if tb_min > tb_max {
                return;
            }
            let tb = tb_min;
            let worst_pnl = back_worst_pnl(liability, s, odds_b, odds_c, tb, tc);
            if worst_pnl < -slack {
                return;
            }
            choose_better_candidate(best, tb, tc, worst_pnl);
        };

        let stride = scan_stride(scan_c_end);
        scan_range_with_stride(0, scan_c_end, stride, |tc| eval_tc(tc, &mut best));
        if stride > 1 {
            let offset = stride / 2;
            if offset > 0 {
                scan_range_with_stride(offset.min(scan_c_end), scan_c_end, stride, |tc| {
                    eval_tc(tc, &mut best)
                });
            }
            let center = best
                .map(|(_, tc, _)| tc)
                .unwrap_or(min_hedge_required.clamp(0, scan_c_end));
            let lo = center.saturating_sub(stride).max(0);
            let hi = center.saturating_add(stride).min(scan_c_end);
            scan_range_with_stride(lo, hi, 1, |tc| eval_tc(tc, &mut best));
        }
    }

    let Some((tb, tc, worst_pnl)) = best else {
        return HedgeResult::NoSolution {
            reason: "No valid hedge exists for BACK order",
        };
    };

    HedgeResult::Success {
        legs: vec![
            HedgeLeg {
                runner_id: runner_b,
                odds: odds_b,
                stake: Money(tb),
                side: Side::No, // House LAYs other runners
            },
            HedgeLeg {
                runner_id: runner_c,
                odds: odds_c,
                stake: Money(tc),
                side: Side::No,
            },
        ],
        worst_case_pnl: Money(worst_pnl),
    }
}

/// Calculate hedge for a user LAY order.
///
/// User lays runner A at odds `a` for stake `s`.
/// House BACKs A (matches user), then BACKs other runners as hedge.
///
/// P&L:
/// - P_A = s*(a-1) - tB - tC  (house wins profit, loses hedge stakes)
/// - P_B = -s + tB*(oB-1) - tC  (house loses user stake, wins on B, loses tC)
/// - P_C = -s - tB + tC*(oC-1)  (house loses user stake, loses tB, wins on C)
///
/// Constraints (MINIMUMS - must hedge enough):
/// - tB + tC <= s*(a-1)  (can't spend more than A-win profit)
/// - tB >= (s + tC - slack) / (oB - 1)
/// - tC >= (s + tB - slack) / (oC - 1)
fn calculate_lay_hedge(input: &HedgeInput) -> HedgeResult {
    let (runner_b, odds_b, size_b) = input.other_runners[0];
    let (runner_c, odds_c, size_c) = input.other_runners[1];

    if odds_b.0 <= 10_000 || odds_c.0 <= 10_000 {
        return HedgeResult::NoSolution {
            reason: "Invalid odds (must be > 1.0)",
        };
    }

    let s = input.user_stake.0;
    let slack = input.max_loss.0.max(0);
    let size_b = size_b.0.max(0);
    let size_c = size_c.0.max(0);
    let liability = profit_quanta(input.user_stake, input.user_odds).max(0);

    // If the unhedged position is already within tolerance, accept zero hedge.
    let unhedged_worst = liability.min(-s);
    if unhedged_worst >= -slack {
        return HedgeResult::Success {
            legs: vec![
                HedgeLeg {
                    runner_id: runner_b,
                    odds: odds_b,
                    stake: Money(0),
                    side: Side::Yes,
                },
                HedgeLeg {
                    runner_id: runner_c,
                    odds: odds_c,
                    stake: Money(0),
                    side: Side::Yes,
                },
            ],
            worst_case_pnl: Money(unhedged_worst),
        };
    }

    let mut best: Option<(i64, i64, i64)> = None; // (tb, tc, worst_pnl)
    let max_total_hedge = liability.saturating_add(slack);
    let scan_b_end = size_b.min(max_total_hedge);
    let scan_c_end = size_c.min(max_total_hedge);
    let scan_tb = scan_b_end <= scan_c_end;

    if scan_tb {
        let eval_tb = |tb: i64, best: &mut Option<(i64, i64, i64)>| {
            let tc_min =
                min_stake_for_profit_at_least(s.saturating_add(tb).saturating_sub(slack), odds_c);
            let tc_max = size_c
                .min(liability.saturating_add(slack).saturating_sub(tb))
                .min(
                    profit_quanta(Money(tb), odds_b)
                        .saturating_add(slack)
                        .saturating_sub(s),
                );
            if tc_min > tc_max {
                return;
            }
            let tc = tc_min.max(0);
            if tc > tc_max {
                return;
            }
            let worst_pnl = lay_worst_pnl(liability, s, odds_b, odds_c, tb, tc);
            if worst_pnl < -slack {
                return;
            }
            choose_better_candidate(best, tb, tc, worst_pnl);
        };

        let stride = scan_stride(scan_b_end);
        scan_range_with_stride(0, scan_b_end, stride, |tb| eval_tb(tb, &mut best));
        if stride > 1 {
            let offset = stride / 2;
            if offset > 0 {
                scan_range_with_stride(offset.min(scan_b_end), scan_b_end, stride, |tb| {
                    eval_tb(tb, &mut best)
                });
            }
            let center = best.map(|(tb, _, _)| tb).unwrap_or(0);
            let lo = center.saturating_sub(stride).max(0);
            let hi = center.saturating_add(stride).min(scan_b_end);
            scan_range_with_stride(lo, hi, 1, |tb| eval_tb(tb, &mut best));
        }
    } else {
        let eval_tc = |tc: i64, best: &mut Option<(i64, i64, i64)>| {
            let tb_min =
                min_stake_for_profit_at_least(s.saturating_add(tc).saturating_sub(slack), odds_b);
            let tb_max = size_b
                .min(liability.saturating_add(slack).saturating_sub(tc))
                .min(
                    profit_quanta(Money(tc), odds_c)
                        .saturating_add(slack)
                        .saturating_sub(s),
                );
            if tb_min > tb_max {
                return;
            }
            let tb = tb_min.max(0);
            if tb > tb_max {
                return;
            }
            let worst_pnl = lay_worst_pnl(liability, s, odds_b, odds_c, tb, tc);
            if worst_pnl < -slack {
                return;
            }
            choose_better_candidate(best, tb, tc, worst_pnl);
        };

        let stride = scan_stride(scan_c_end);
        scan_range_with_stride(0, scan_c_end, stride, |tc| eval_tc(tc, &mut best));
        if stride > 1 {
            let offset = stride / 2;
            if offset > 0 {
                scan_range_with_stride(offset.min(scan_c_end), scan_c_end, stride, |tc| {
                    eval_tc(tc, &mut best)
                });
            }
            let center = best.map(|(_, tc, _)| tc).unwrap_or(0);
            let lo = center.saturating_sub(stride).max(0);
            let hi = center.saturating_add(stride).min(scan_c_end);
            scan_range_with_stride(lo, hi, 1, |tc| eval_tc(tc, &mut best));
        }
    }

    let Some((tb, tc, worst_pnl)) = best else {
        return HedgeResult::NoSolution {
            reason: "No valid hedge exists for LAY order",
        };
    };

    HedgeResult::Success {
        legs: vec![
            HedgeLeg {
                runner_id: runner_b,
                odds: odds_b,
                stake: Money(tb),
                side: Side::Yes, // House BACKs other runners
            },
            HedgeLeg {
                runner_id: runner_c,
                odds: odds_c,
                stake: Money(tc),
                side: Side::Yes,
            },
        ],
        worst_case_pnl: Money(worst_pnl),
    }
}

fn choose_better_candidate(best: &mut Option<(i64, i64, i64)>, tb: i64, tc: i64, worst_pnl: i64) {
    let total = tb.saturating_add(tc);
    match best {
        None => *best = Some((tb, tc, worst_pnl)),
        Some((best_tb, best_tc, best_worst)) => {
            let best_total = best_tb.saturating_add(*best_tc);
            if worst_pnl > *best_worst || (worst_pnl == *best_worst && total < best_total) {
                *best = Some((tb, tc, worst_pnl));
            }
        }
    }
}

fn max_stake_for_profit_limit(limit: i64, odds: OddsX10000) -> i64 {
    if limit < 0 {
        return -1;
    }
    let k = odds.0 as i128 - 10_000;
    if k <= 0 {
        return i64::MAX;
    }
    let num = ((limit as i128 + 1) * 10_000).saturating_sub(1);
    clamp_i128_to_i64(num / k)
}

fn min_stake_for_profit_at_least(required: i64, odds: OddsX10000) -> i64 {
    if required <= 0 {
        return 0;
    }
    let k = odds.0 as i128 - 10_000;
    if k <= 0 {
        return i64::MAX;
    }
    let num = (required as i128) * 10_000;
    clamp_i128_to_i64((num + k - 1) / k)
}

fn back_worst_pnl(
    liability: i64,
    user_stake: i64,
    odds_b: OddsX10000,
    odds_c: OddsX10000,
    tb: i64,
    tc: i64,
) -> i64 {
    let pnl_a = -liability + tb + tc;
    let pnl_b = user_stake + tc - profit_quanta(Money(tb), odds_b);
    let pnl_c = user_stake + tb - profit_quanta(Money(tc), odds_c);
    pnl_a.min(pnl_b).min(pnl_c)
}

fn lay_worst_pnl(
    liability: i64,
    user_stake: i64,
    odds_b: OddsX10000,
    odds_c: OddsX10000,
    tb: i64,
    tc: i64,
) -> i64 {
    let pnl_a = liability - tb - tc;
    let pnl_b = -user_stake + profit_quanta(Money(tb), odds_b) - tc;
    let pnl_c = -user_stake - tb + profit_quanta(Money(tc), odds_c);
    pnl_a.min(pnl_b).min(pnl_c)
}
