//! Off-chain instruction builders (Ore **`api::sdk`** style): fixed account order mirrors on-chain **`process_*`** comment headers.
//!
//! For **`ClaimPositionFee`**, pass the **`cp\_amm`** account tail (13 metas, Anchor `ClaimPositionFeeCtx` order).

use solana_program::pubkey::Pubkey;
use spl_associated_token_account::{
    get_associated_token_address,
    get_associated_token_address_with_program_id,
};
use steel::*;

use crate::{
    consts::{
        EXECUTOR_ADDRESS, EXECUTOR_PAY_DAILY_JACKPOT, EXECUTOR_PAY_WEEKLY_JACKPOT, FEE_COLLECTOR,
        USDC_MAINNET_MINT,
    },
    instruction::*,
    state::{Ledger, Market, Position, Treasury},
};

#[inline(always)]
pub fn program_id() -> Pubkey {
    crate::ID
}

#[inline(always)]
fn executor_treasury_ix_bytes(kind: u8, pool: u8, series_id: u16, amount: u64) -> Vec<u8> {
    ExecutorTreasury {
        kind,
        pool,
        series_id: series_id.to_le_bytes(),
        _pad_ix: [0; 4],
        amount: amount.to_le_bytes(),
    }
    .to_bytes()
}

/// SPL USDC ATA (**classic** SPL Token program). For Token-2022 mints use [`treasury_usdc_ata_with_token_program`] / [`associated_usdc_ata`] with the mint owner's program id (for example **`spl_token_2022`**'s **`id()`**).
#[inline(always)]
pub fn treasury_usdc_ata() -> Pubkey {
    get_associated_token_address(&Treasury::pda().0, &USDC_MAINNET_MINT)
}

#[inline(always)]
pub fn treasury_usdc_ata_with_token_program(token_program: &Pubkey) -> Pubkey {
    get_associated_token_address_with_program_id(&Treasury::pda().0, &USDC_MAINNET_MINT, token_program)
}

#[inline(always)]
pub fn associated_usdc_ata(owner: Pubkey, token_program: &Pubkey) -> Pubkey {
    get_associated_token_address_with_program_id(&owner, &USDC_MAINNET_MINT, token_program)
}

/// SPL USDC ATA for **`owner`** (classic Token program — mainnet Circle USDC).
#[inline(always)]
pub fn usdc_ata(owner: Pubkey) -> Pubkey {
    get_associated_token_address(&owner, &USDC_MAINNET_MINT)
}

/// Team-fee SPL destination used by **`BuyTickets`** / **`ClaimPositionFee`** (classic Token mint).
#[inline(always)]
pub fn fee_collector_usdc_ata() -> Pubkey {
    get_associated_token_address(&FEE_COLLECTOR, &USDC_MAINNET_MINT)
}

#[inline(always)]
pub fn fee_collector_usdc_ata_with_token_program(token_program: &Pubkey) -> Pubkey {
    get_associated_token_address_with_program_id(&FEE_COLLECTOR, &USDC_MAINNET_MINT, token_program)
}

/// **`previous_market`** / **`previous_position`** (**`series_id`**, **`period - 1`**). For **`period == 0`** use placeholders (**no signer**).
#[inline(always)]
pub fn buy_tickets_or_place_bet_previous_accounts(
    user: Pubkey,
    series_id: u16,
    period: u64,
) -> (Pubkey, Pubkey) {
    match period.checked_sub(1) {
        Some(p) => (
            Market::pda(series_id, p).0,
            Position::pda(series_id, p, user).0,
        ),
        None => (system_program::ID, system_program::ID),
    }
}

// let [payer_info, treasury_info, mint_info, treasury_ata, system_program, token_program, ata_program]

/// One-time **`Initialize`**. **`payer`** must be [`ADMIN_ADDRESS`](crate::consts::ADMIN_ADDRESS).
///
/// **`token_program`** must be the mint owner's program id (`spl_token` or `spl_token_2022` for the configured USDC mint).
pub fn initialize(payer: Pubkey, token_program: Pubkey) -> Instruction {
    let treasury_address = Treasury::pda().0;
    let mint_address = USDC_MAINNET_MINT;
    let treasury_ata = treasury_usdc_ata_with_token_program(&token_program);
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(payer, true),
            AccountMeta::new(treasury_address, false),
            AccountMeta::new_readonly(mint_address, false),
            AccountMeta::new(treasury_ata, false),
            AccountMeta::new_readonly(system_program::ID, false),
            AccountMeta::new_readonly(token_program, false),
            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
        ],
        data: Initialize {}.to_bytes(),
    }
}

// let [user, treasury_info, ledger_info, mint_info, user_ata, treasury_ata, team_ata, system_program, token_program, ata_program, previous_market_info, previous_position_info]

/// Builds **`BuyTickets`**.
pub fn buy_tickets(
    user: Pubkey,
    series_id: u16,
    period: u64,
    amount: u64,
    token_program: Pubkey,
) -> Instruction {
    let treasury_address = Treasury::pda().0;
    let ledger_address = Ledger::pda(user).0;
    let mint_address = USDC_MAINNET_MINT;
    let user_ata = associated_usdc_ata(user, &token_program);
    let treasury_ata_addr = treasury_usdc_ata_with_token_program(&token_program);
    let team_ata_addr = fee_collector_usdc_ata_with_token_program(&token_program);
    let (prev_mkt, prev_pos) =
        buy_tickets_or_place_bet_previous_accounts(user, series_id, period);
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(user, true),
            AccountMeta::new(treasury_address, false),
            AccountMeta::new(ledger_address, false),
            AccountMeta::new_readonly(mint_address, false),
            AccountMeta::new(user_ata, false),
            AccountMeta::new(treasury_ata_addr, false),
            AccountMeta::new(team_ata_addr, false),
            AccountMeta::new_readonly(system_program::ID, false),
            AccountMeta::new_readonly(token_program, false),
            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
            AccountMeta::new_readonly(prev_mkt, false),
            AccountMeta::new_readonly(prev_pos, false),
        ],
        data: BuyTickets {
            series_id: series_id.to_le_bytes(),
            _pad_ix: [0; 6],
            period: period.to_le_bytes(),
            amount: amount.to_le_bytes(),
        }
        .to_bytes(),
    }
}

// let [user, ledger_info, market_info, position_info, previous_market_info, previous_position_info, system_program]

/// Builds **`PlaceBet`**.
///
/// **`ticket_units`** may be **0** in the live window to activate a **`Position::STATE_COMMITTED_PREOPEN`** stake (**merge into `Market::total_*`** without debiting **`Ledger`**). See **`PlaceBet`** ix docs on-chain.
pub fn place_bet(
    user: Pubkey,
    series_id: u16,
    period: u64,
    ticket_units: u64,
    side: u8,
) -> Instruction {
    let ledger_address = Ledger::pda(user).0;
    let treasury_address = Treasury::pda().0;
    let market_address = Market::pda(series_id, period).0;
    let position_address = Position::pda(series_id, period, user).0;
    let (prev_mkt, prev_pos) =
        buy_tickets_or_place_bet_previous_accounts(user, series_id, period);
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(user, true),
            AccountMeta::new(ledger_address, false),
            AccountMeta::new_readonly(treasury_address, false),
            AccountMeta::new(market_address, false),
            AccountMeta::new(position_address, false),
            AccountMeta::new_readonly(prev_mkt, false),
            AccountMeta::new_readonly(prev_pos, false),
            AccountMeta::new_readonly(system_program::ID, false),
        ],
        data: PlaceBet {
            series_id: series_id.to_le_bytes(),
            _pad_ix: [0; 6],
            period: period.to_le_bytes(),
            ticket_units: ticket_units.to_le_bytes(),
            side,
            _pad: [0; 7],
        }
        .to_bytes(),
    }
}

// let [executor, treasury_readonly, market, position]

/// **`ExecutorTreasury`** **`EXECUTOR_KIND_MERGE_COMMITTED_POSITION`**: merges **[`crate::state::Position::STATE_COMMITTED_PREOPEN`]** → **`STATE_PENDING`** + **`Market::total_*`**. **`pool`** **`0`**, **`amount`** **`0`**, **`series_id`** in payload.
pub fn executor_treasury_merge_committed(
    executor: Pubkey,
    series_id: u16,
    period: u64,
    position_owner: Pubkey,
) -> Instruction {
    let treasury_address = Treasury::pda().0;
    let market_address = Market::pda(series_id, period).0;
    let position_address = Position::pda(series_id, period, position_owner).0;
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(executor, true),
            AccountMeta::new_readonly(treasury_address, false),
            AccountMeta::new(market_address, false),
            AccountMeta::new(position_address, false),
        ],
        data: executor_treasury_ix_bytes(
            EXECUTOR_KIND_MERGE_COMMITTED_POSITION,
            0,
            series_id,
            0,
        ),
    }
}

/// Builds **`AdminInstantSettlement`**. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`].
///
/// **`price_update_account`**: address of the Hermes `PriceUpdateV2` account posted just before
/// this instruction by the executor via the Pyth Receiver program. Subsidy is computed on-chain.
pub fn settle_market(
    authority: Pubkey,
    series_id: u16,
    period: u64,
    price_update_account: Pubkey,
) -> Instruction {
    let treasury_address = Treasury::pda().0;
    let market_address = Market::pda(series_id, period).0;
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(treasury_address, false),
            AccountMeta::new(market_address, false),
            AccountMeta::new_readonly(price_update_account, false),
        ],
        data: AdminInstantSettlement {
            series_id: series_id.to_le_bytes(),
            _pad_ix: [0; 6],
            period: period.to_le_bytes(),
        }
        .to_bytes(),
    }
}

/// Builds **`InitMarket`**. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`].
///
/// **`feed_id`**: Pyth Hermes price feed identifier (32 bytes) stored in `Market::pyth_price_feed`.
/// Use [`crate::consts::PYTH_BTC_USD_FEED_ID`] for BTC/USD.
///
/// **`price_update_account`**: address of a current Hermes `PriceUpdateV2` account for the
/// open-anchor snapshot. Pass `system_program::ID` when creating the market ahead of `open_ts`
/// and crank separately once inside the anchor window.
pub fn init_market(
    authority: Pubkey,
    series_id: u16,
    period: u64,
    open_ts: i64,
    close_ts: i64,
    feed_id: [u8; 32],
    price_update_account: Pubkey,
    rules_hash: [u8; 32],
) -> Instruction {
    let market_address = Market::pda(series_id, period).0;
    let treasury_address = Treasury::pda().0;
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(market_address, false),
            AccountMeta::new_readonly(treasury_address, false),
            AccountMeta::new_readonly(system_program::ID, false),
            AccountMeta::new_readonly(price_update_account, false),
        ],
        data: InitMarket {
            series_id: series_id.to_le_bytes(),
            _pad_ix: [0; 6],
            period: period.to_le_bytes(),
            open_ts: open_ts.to_le_bytes(),
            close_ts: close_ts.to_le_bytes(),
            pyth_price_feed: feed_id,
            rules_hash,
        }
        .to_bytes(),
    }
}

/// Builds **`AdminVoidMarket`**. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`] or
/// [`crate::consts::ADMIN_ADDRESS`].
///
/// Call when BTC price movement is < 0.01% (`BTC_VOID_THRESHOLD_BPS`) or on oracle failure.
/// All `STATE_PENDING` positions must subsequently be refunded via [`refund_void_position`].
pub fn void_market(authority: Pubkey, series_id: u16, period: u64) -> Instruction {
    let treasury_address = Treasury::pda().0;
    let market_address = Market::pda(series_id, period).0;
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new_readonly(treasury_address, false),
            AccountMeta::new(market_address, false),
        ],
        data: AdminVoidMarket {
            series_id: series_id.to_le_bytes(),
            _pad_ix: [0; 6],
            period: period.to_le_bytes(),
        }
        .to_bytes(),
    }
}

/// Builds **`AdminRefundVoidPosition`**. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`].
///
/// Refunds `position_owner`'s `STATE_PENDING` position in a voided market back to their `Ledger`.
/// Iterate all stakers and call this once per position.
pub fn refund_void_position(
    authority: Pubkey,
    series_id: u16,
    period: u64,
    position_owner: Pubkey,
) -> Instruction {
    let market_address = Market::pda(series_id, period).0;
    let position_address = Position::pda(series_id, period, position_owner).0;
    let ledger_address = Ledger::pda(position_owner).0;
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new_readonly(market_address, false),
            AccountMeta::new(position_address, false),
            AccountMeta::new(ledger_address, false),
        ],
        data: AdminRefundVoidPosition {
            series_id: series_id.to_le_bytes(),
            _pad_ix: [0; 6],
            period: period.to_le_bytes(),
        }
        .to_bytes(),
    }
}

// let [executor, treasury_info, treasury_ata, recipient_ata, mint_info, token_program, market_info, position_info, ledger_info]

/// **`recipient_ata`** must be **[`associated_usdc_ata`](associated_usdc_ata)(winning_authority, &token_program)**.
pub fn distribute_market_reward(
    executor: Pubkey,
    winning_authority: Pubkey,
    series_id: u16,
    market_period: u64,
    token_program: Pubkey,
) -> Instruction {
    let treasury_address = Treasury::pda().0;
    let treasury_ata_addr = treasury_usdc_ata_with_token_program(&token_program);
    let recipient_ata = associated_usdc_ata(winning_authority, &token_program);
    let mint_address = USDC_MAINNET_MINT;
    let market_address = Market::pda(series_id, market_period).0;
    let position_address = Position::pda(series_id, market_period, winning_authority).0;
    let ledger_address = Ledger::pda(winning_authority).0;
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(executor, true),
            AccountMeta::new(treasury_address, false),
            AccountMeta::new(treasury_ata_addr, false),
            AccountMeta::new(recipient_ata, false),
            AccountMeta::new_readonly(mint_address, false),
            AccountMeta::new_readonly(token_program, false),
            AccountMeta::new_readonly(market_address, false),
            AccountMeta::new(position_address, false),
            AccountMeta::new(ledger_address, false),
        ],
        data: executor_treasury_ix_bytes(EXECUTOR_KIND_DISTRIBUTE_MARKET_REWARD, 0, series_id, 0),
    }
}

/// Convenience: fixed executor pubkey ([`EXECUTOR_ADDRESS`](crate::consts::EXECUTOR_ADDRESS)).
pub fn distribute_market_reward_default_executor(
    winning_authority: Pubkey,
    series_id: u16,
    market_period: u64,
    token_program: Pubkey,
) -> Instruction {
    distribute_market_reward(
        EXECUTOR_ADDRESS,
        winning_authority,
        series_id,
        market_period,
        token_program,
    )
}

/// **`ExecutorTreasury`** **`EXECUTOR_KIND_MARKET_LINE_RELEASE`**: after **`DISTRIBUTE`** batch(es), **`Treasury`** advances **`next_period[slot]`** toward **`≥ settled_period + 1`** (**`max`** with prior cursor).
///
/// **Accounts:** **`executor`**, **`treasury`**, **`market`** (readonly).
pub fn release_market_line(executor: Pubkey, series_id: u16, period: u64) -> Instruction {
    let treasury_address = Treasury::pda().0;
    let market_address = Market::pda(series_id, period).0;
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(executor, true),
            AccountMeta::new(treasury_address, false),
            AccountMeta::new_readonly(market_address, false),
        ],
        data: executor_treasury_ix_bytes(EXECUTOR_KIND_MARKET_LINE_RELEASE, 0, series_id, period),
    }
}

#[inline(always)]
pub fn release_market_line_default_executor(series_id: u16, period: u64) -> Instruction {
    release_market_line(EXECUTOR_ADDRESS, series_id, period)
}

// let [executor, treasury_info, treasury_ata, mint_info, token_program, recipient_ata]

/// **`pool`**: [`EXECUTOR_PAY_DAILY_JACKPOT`](crate::consts::EXECUTOR_PAY_DAILY_JACKPOT) or [`EXECUTOR_PAY_WEEKLY_JACKPOT`](crate::consts::EXECUTOR_PAY_WEEKLY_JACKPOT).
///
/// **`recipient_ata`** is any SPL token account holding USDC (**mint + program-id** enforced on-chain); use **[`associated_usdc_ata`]** when building the ATA address.
pub fn executor_treasury_pay_recipient_account(
    executor: Pubkey,
    recipient_ata: Pubkey,
    amount: u64,
    pool: u8,
    token_program: Pubkey,
) -> Instruction {
    let treasury_address = Treasury::pda().0;
    let treasury_ata_addr = treasury_usdc_ata_with_token_program(&token_program);
    let mint_address = USDC_MAINNET_MINT;
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(executor, true),
            AccountMeta::new(treasury_address, false),
            AccountMeta::new(treasury_ata_addr, false),
            AccountMeta::new_readonly(mint_address, false),
            AccountMeta::new_readonly(token_program, false),
            AccountMeta::new(recipient_ata, false),
        ],
        data: executor_treasury_ix_bytes(EXECUTOR_KIND_JACKPOT_PAY, pool, 0, amount),
    }
}

/// Same as **`executor_treasury_pay_recipient_account`** with **`recipient_ata`** = **[`associated_usdc_ata`](associated_usdc_ata)(recipient_owner, token_program)**.
#[inline(always)]
pub fn executor_treasury_pay(
    executor: Pubkey,
    recipient_owner: Pubkey,
    amount: u64,
    pool: u8,
    token_program: Pubkey,
) -> Instruction {
    executor_treasury_pay_recipient_account(
        executor,
        associated_usdc_ata(recipient_owner, &token_program),
        amount,
        pool,
        token_program,
    )
}

/// Daily jackpot pool preset.
#[inline(always)]
pub fn executor_treasury_pay_daily(
    executor: Pubkey,
    recipient_owner: Pubkey,
    amount: u64,
    token_program: Pubkey,
) -> Instruction {
    executor_treasury_pay_recipient_account(
        executor,
        associated_usdc_ata(recipient_owner, &token_program),
        amount,
        EXECUTOR_PAY_DAILY_JACKPOT,
        token_program,
    )
}

/// Weekly jackpot pool preset.
#[inline(always)]
pub fn executor_treasury_pay_weekly(
    executor: Pubkey,
    recipient_owner: Pubkey,
    amount: u64,
    token_program: Pubkey,
) -> Instruction {
    executor_treasury_pay_recipient_account(
        executor,
        associated_usdc_ata(recipient_owner, &token_program),
        amount,
        EXECUTOR_PAY_WEEKLY_JACKPOT,
        token_program,
    )
}

// Streak indices 0..8: executor, owner, treasury, treasury_ata, fee_collector_ata, mint, system_program, token_program, owner_usdc_ata
// Indices 9..22: cp_amm ClaimPositionFeeCtx (see `programs/program/src/admin/claim_position_fee.rs`)

/// CPI prefix metas only; caller appends **`cp\_amm`** accounts.
pub fn claim_position_fee_accounts_prefix(
    owner: Pubkey,
    owner_usdc_ata: Pubkey,
    token_program: Pubkey,
) -> [AccountMeta; 9] {
    let executor = EXECUTOR_ADDRESS;
    let treasury_address = Treasury::pda().0;
    let treasury_ata_addr = treasury_usdc_ata_with_token_program(&token_program);
    let team_ata_addr = fee_collector_usdc_ata_with_token_program(&token_program);
    [
        AccountMeta::new(executor, true),
        AccountMeta::new(owner, true),
        AccountMeta::new(treasury_address, false),
        AccountMeta::new(treasury_ata_addr, false),
        AccountMeta::new(team_ata_addr, false),
        AccountMeta::new_readonly(USDC_MAINNET_MINT, false),
        AccountMeta::new_readonly(system_program::ID, false),
        AccountMeta::new_readonly(token_program, false),
        AccountMeta::new(owner_usdc_ata, false),
    ]
}

pub fn claim_position_fee(
    owner: Pubkey,
    owner_usdc_ata: Pubkey,
    cp_amm_accounts: &[AccountMeta],
    token_program: Pubkey,
) -> Instruction {
    let prefix = claim_position_fee_accounts_prefix(owner, owner_usdc_ata, token_program);
    let mut accounts = Vec::with_capacity(9 + cp_amm_accounts.len());
    accounts.extend_from_slice(&prefix);
    accounts.extend_from_slice(cp_amm_accounts);
    Instruction {
        program_id: crate::ID,
        accounts,
        data: ClaimPositionFee {}.to_bytes(),
    }
}

/// Advance `Treasury::current_week` by 1. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`].
///
/// Call once at the start of each scoring week. All `Ledger` weekly stats reset lazily on next
/// interaction — no sweep required.
pub fn admin_init_week(authority: Pubkey) -> Instruction {
    let treasury_address = Treasury::pda().0;
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(treasury_address, false),
        ],
        data: AdminInitWeek {}.to_bytes(),
    }
}
