use anchor_lang::AccountDeserialize;
use pyth_solana_receiver_sdk::error::GetPriceError;
use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, FeedId, PriceUpdateV2};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    clock::Clock,
    entrypoint,
    entrypoint::ProgramResult,
    program::{invoke, invoke_signed},
    program_error::ProgramError,
    pubkey::Pubkey,
    rent::Rent,
    system_instruction, system_program,
    sysvar::Sysvar,
};
use std::str::FromStr;

pub mod settings;

solana_program::declare_id!("c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW");
entrypoint!(process_instruction);

const IX_INIT: u8 = 1;
const IX_UPDATE_COEF_LIMIT: u8 = 2;
const IX_GRANT_MANAGER_LIMITS: u8 = 3;
const IX_BUY_TICKET: u8 = 4;
const IX_BUY_TICKET_USD: u8 = 5;
const IX_BUY_TICKET_SOL: u8 = 6;
const IX_MANAGER_ADD_TICKET: u8 = 7;
const IX_STEP_PAYOUT: u8 = 8;
const IX_CHANGE_TICKET_RECIPIENT: u8 = 9;

#[repr(u32)]
#[derive(Clone, Copy, Debug)]
enum PaymentsError {
    InvalidInstruction = 1,
    InvalidSigner = 2,
    InvalidPdaAddress = 3,
    EmptyState = 4,
    InvalidAccountData = 5,
    InvalidSettingsWallet = 6,
    InvalidInflowVault = 7,
    InvalidDaoWallet = 8,
    UnauthorizedManager = 9,
    UnauthorizedDao = 10,
    InvalidCoefficient = 11,
    InvalidLimit = 12,
    InvalidCallReward = 13,
    InvalidAmount = 14,
    QueueTemporarilyPaused = 15,
    InvalidPayoutAmount = 16,
    NotEnoughInflowForStep = 17,
    TicketAlreadyPaid = 18,
    InvalidTicketRecipient = 19,
    InvalidTicketIndex = 20,
    InvalidTicketQueue = 21,
    InvalidManagerWallet = 22,
    ManagerLimitExceeded = 23,
    UnauthorizedTicketOwner = 24,
    CannotChangeRecipientForNextPayoutTicket = 25,
    InvalidOracleAccount = 26,
    InvalidOracleFeed = 27,
    InvalidOracleFeedConfig = 28,
    OraclePriceTooOld = 29,
    InvalidOraclePrice = 30,
    SlippageExceeded = 31,
    MathOverflow = 32,
    SystemAlreadyInitialized = 33,
    MissingRequiredSignature = 34,
    InvalidSystemProgram = 35,
    PdaAlreadyExists = 36,
}

impl From<PaymentsError> for ProgramError {
    fn from(value: PaymentsError) -> Self {
        ProgramError::Custom(value as u32)
    }
}

macro_rules! require {
    ($cond:expr, $err:expr) => {
        if !($cond) {
            return Err(ProgramError::from($err));
        }
    };
}

macro_rules! require_keys_eq {
    ($left:expr, $right:expr, $err:expr) => {
        if $left != $right {
            return Err(ProgramError::from($err));
        }
    };
}

#[derive(Clone, Debug)]
pub struct UpdateCoefLimitArgs {
    pub coef_ppm: u64,
    pub limit_usd_cents: u64,
    pub call_reward_lamports: u64,
}

#[derive(Clone, Debug)]
pub struct GrantManagerLimitsArgs {
    pub manager_wallet: Pubkey,
    pub add_q1_usd_cents: u64,
    pub add_q2_usd_cents: u64,
    pub add_q3_usd_cents: u64,
}

#[derive(Clone, Debug)]
pub struct BuyTicketArgs {
    pub amount_lamports: u64,
    pub recipient_wallet: Pubkey,
}

#[derive(Clone, Debug)]
pub struct BuyTicketUsdArgs {
    pub amount_usd_cents: u64,
    pub max_pay_lamports: u64,
    pub recipient_wallet: Pubkey,
}

#[derive(Clone, Debug)]
pub struct BuyTicketSolArgs {
    pub amount_lamports: u64,
    pub min_expected_usd_cents: u64,
    pub recipient_wallet: Pubkey,
}

#[derive(Clone, Debug)]
pub struct ManagerAddTicketArgs {
    pub queue_id: u8,
    pub recipient_wallet: Pubkey,
    pub payout_usd_cents: u64,
}

#[derive(Clone, Debug)]
pub struct ChangeTicketRecipientArgs {
    pub new_recipient_wallet: Pubkey,
}

#[derive(Clone, Debug)]
pub struct ConfigState {
    pub version: u8,
    pub dao_wallet: Pubkey,
    pub inflow_vault: Pubkey,
}

#[derive(Clone, Debug)]
pub struct CoefLimitState {
    pub version: u8,
    pub coef_ppm: u64,
    pub limit_usd_cents: u64,
    pub call_reward_lamports: u64,
}

#[derive(Clone, Debug)]
pub struct QueuesState {
    pub version: u8,
    pub q1_tickets_total: u64,
    pub q1_tickets_paid: u64,
    pub q1_sum_total_usd_cents: u64,
    pub q1_sum_paid_usd_cents: u64,
    pub q2_tickets_total: u64,
    pub q2_tickets_paid: u64,
    pub q2_sum_total_usd_cents: u64,
    pub q2_sum_paid_usd_cents: u64,
    pub q3_tickets_total: u64,
    pub q3_tickets_paid: u64,
    pub q3_sum_total_usd_cents: u64,
    pub q3_sum_paid_usd_cents: u64,
}

#[derive(Clone, Debug)]
pub struct TicketState {
    pub version: u8,
    pub queue_id: u8,
    pub index: u64,
    pub is_paid: bool,
    pub recipient_wallet: Pubkey,
    pub payout_usd_cents: u64,
    pub debt_before_usd_cents: u64,
}

#[derive(Clone, Debug)]
pub struct ManagerAllowanceState {
    pub version: u8,
    pub manager_wallet: Pubkey,
    pub q1_available_usd_cents: u64,
    pub q2_available_usd_cents: u64,
    pub q3_available_usd_cents: u64,
}

#[derive(Clone, Debug)]
pub struct VaultState {
    pub version: u8,
}

struct SolUsdPrice {
    price_num: u128,
    price_den: u128,
}

struct Reader<'a> {
    data: &'a [u8],
    cursor: usize,
    error: ProgramError,
}

impl<'a> Reader<'a> {
    fn new(data: &'a [u8], error: ProgramError) -> Self {
        Self { data, cursor: 0, error }
    }

    fn read_u8(&mut self) -> Result<u8, ProgramError> {
        let value = *self
            .data
            .get(self.cursor)
            .ok_or(self.error.clone())?;
        self.cursor += 1;
        Ok(value)
    }

    fn read_u64(&mut self) -> Result<u64, ProgramError> {
        let end = self.cursor.checked_add(8).ok_or(self.error.clone())?;
        let slice = self
            .data
            .get(self.cursor..end)
            .ok_or(self.error.clone())?;
        self.cursor = end;
        Ok(u64::from_le_bytes(
            slice.try_into().map_err(|_| self.error.clone())?,
        ))
    }

    fn read_pubkey(&mut self) -> Result<Pubkey, ProgramError> {
        let end = self.cursor.checked_add(32).ok_or(self.error.clone())?;
        let slice = self
            .data
            .get(self.cursor..end)
            .ok_or(self.error.clone())?;
        self.cursor = end;
        Ok(Pubkey::new_from_array(
            slice.try_into().map_err(|_| self.error.clone())?,
        ))
    }

    fn finish(self) -> Result<(), ProgramError> {
        require!(self.cursor == self.data.len(), PaymentsError::InvalidInstruction);
        Ok(())
    }
}

trait StateCodec: Sized {
    fn encoded_len() -> usize;
    fn encode(&self) -> Vec<u8>;
    fn decode(data: &[u8]) -> Result<Self, ProgramError>;
}

impl StateCodec for ConfigState {
    fn encoded_len() -> usize {
        1 + 32 + 32
    }

    fn encode(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(65);
        out.push(self.version);
        out.extend_from_slice(self.dao_wallet.as_ref());
        out.extend_from_slice(self.inflow_vault.as_ref());
        out
    }

    fn decode(data: &[u8]) -> Result<Self, ProgramError> {
        let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
        let version = reader.read_u8()?;
        let dao_wallet = reader.read_pubkey()?;
        let inflow_vault = reader.read_pubkey()?;
        require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData);
        Ok(Self { version, dao_wallet, inflow_vault })
    }
}

impl StateCodec for CoefLimitState {
    fn encoded_len() -> usize {
        1 + 8 + 8 + 8
    }

    fn encode(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(25);
        out.push(self.version);
        out.extend_from_slice(&self.coef_ppm.to_le_bytes());
        out.extend_from_slice(&self.limit_usd_cents.to_le_bytes());
        out.extend_from_slice(&self.call_reward_lamports.to_le_bytes());
        out
    }

    fn decode(data: &[u8]) -> Result<Self, ProgramError> {
        let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
        let version = reader.read_u8()?;
        let coef_ppm = reader.read_u64()?;
        let limit_usd_cents = reader.read_u64()?;
        let call_reward_lamports = reader.read_u64()?;
        require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData);
        Ok(Self { version, coef_ppm, limit_usd_cents, call_reward_lamports })
    }
}

impl StateCodec for QueuesState {
    fn encoded_len() -> usize {
        1 + 12 * 8
    }

    fn encode(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(97);
        out.push(self.version);
        for value in [
            self.q1_tickets_total,
            self.q1_tickets_paid,
            self.q1_sum_total_usd_cents,
            self.q1_sum_paid_usd_cents,
            self.q2_tickets_total,
            self.q2_tickets_paid,
            self.q2_sum_total_usd_cents,
            self.q2_sum_paid_usd_cents,
            self.q3_tickets_total,
            self.q3_tickets_paid,
            self.q3_sum_total_usd_cents,
            self.q3_sum_paid_usd_cents,
        ] {
            out.extend_from_slice(&value.to_le_bytes());
        }
        out
    }

    fn decode(data: &[u8]) -> Result<Self, ProgramError> {
        let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
        let version = reader.read_u8()?;
        Ok(Self {
            version,
            q1_tickets_total: reader.read_u64()?,
            q1_tickets_paid: reader.read_u64()?,
            q1_sum_total_usd_cents: reader.read_u64()?,
            q1_sum_paid_usd_cents: reader.read_u64()?,
            q2_tickets_total: reader.read_u64()?,
            q2_tickets_paid: reader.read_u64()?,
            q2_sum_total_usd_cents: reader.read_u64()?,
            q2_sum_paid_usd_cents: reader.read_u64()?,
            q3_tickets_total: reader.read_u64()?,
            q3_tickets_paid: reader.read_u64()?,
            q3_sum_total_usd_cents: reader.read_u64()?,
            q3_sum_paid_usd_cents: reader.read_u64()?,
        })
    }
}

impl StateCodec for TicketState {
    fn encoded_len() -> usize {
        1 + 1 + 8 + 1 + 32 + 8 + 8
    }

    fn encode(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(59);
        out.push(self.version);
        out.push(self.queue_id);
        out.extend_from_slice(&self.index.to_le_bytes());
        out.push(u8::from(self.is_paid));
        out.extend_from_slice(self.recipient_wallet.as_ref());
        out.extend_from_slice(&self.payout_usd_cents.to_le_bytes());
        out.extend_from_slice(&self.debt_before_usd_cents.to_le_bytes());
        out
    }

    fn decode(data: &[u8]) -> Result<Self, ProgramError> {
        let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
        let version = reader.read_u8()?;
        let queue_id = reader.read_u8()?;
        let index = reader.read_u64()?;
        let is_paid = match reader.read_u8()? {
            0 => false,
            1 => true,
            _ => return Err(PaymentsError::InvalidAccountData.into()),
        };
        let recipient_wallet = reader.read_pubkey()?;
        let payout_usd_cents = reader.read_u64()?;
        let debt_before_usd_cents = reader.read_u64()?;
        require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData);
        Ok(Self { version, queue_id, index, is_paid, recipient_wallet, payout_usd_cents, debt_before_usd_cents })
    }
}

impl StateCodec for ManagerAllowanceState {
    fn encoded_len() -> usize {
        1 + 32 + 8 + 8 + 8
    }

    fn encode(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(57);
        out.push(self.version);
        out.extend_from_slice(self.manager_wallet.as_ref());
        out.extend_from_slice(&self.q1_available_usd_cents.to_le_bytes());
        out.extend_from_slice(&self.q2_available_usd_cents.to_le_bytes());
        out.extend_from_slice(&self.q3_available_usd_cents.to_le_bytes());
        out
    }

    fn decode(data: &[u8]) -> Result<Self, ProgramError> {
        let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
        let version = reader.read_u8()?;
        let manager_wallet = reader.read_pubkey()?;
        let q1_available_usd_cents = reader.read_u64()?;
        let q2_available_usd_cents = reader.read_u64()?;
        let q3_available_usd_cents = reader.read_u64()?;
        require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData);
        Ok(Self { version, manager_wallet, q1_available_usd_cents, q2_available_usd_cents, q3_available_usd_cents })
    }
}

impl StateCodec for VaultState {
    fn encoded_len() -> usize {
        1
    }

    fn encode(&self) -> Vec<u8> {
        vec![self.version]
    }

    fn decode(data: &[u8]) -> Result<Self, ProgramError> {
        require!(data.len() == 1, PaymentsError::InvalidAccountData);
        Ok(Self { version: data[0] })
    }
}

#[derive(Clone, Debug)]
enum Instruction {
    Init,
    UpdateCoefLimit(UpdateCoefLimitArgs),
    GrantManagerLimits(GrantManagerLimitsArgs),
    BuyTicket(BuyTicketArgs),
    BuyTicketUsd(BuyTicketUsdArgs),
    BuyTicketSol(BuyTicketSolArgs),
    ManagerAddTicket(ManagerAddTicketArgs),
    StepPayout,
    ChangeTicketRecipient(ChangeTicketRecipientArgs),
}

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    match parse_instruction(instruction_data)? {
        Instruction::Init => process_init(program_id, accounts),
        Instruction::UpdateCoefLimit(args) => process_update_coef_limit(program_id, accounts, args),
        Instruction::GrantManagerLimits(args) => process_grant_manager_limits(program_id, accounts, args),
        Instruction::BuyTicket(args) => process_buy_ticket(program_id, accounts, args),
        Instruction::BuyTicketUsd(args) => process_buy_ticket_usd(program_id, accounts, args),
        Instruction::BuyTicketSol(args) => process_buy_ticket_sol(program_id, accounts, args),
        Instruction::ManagerAddTicket(args) => process_manager_add_ticket(program_id, accounts, args),
        Instruction::StepPayout => process_step_payout(program_id, accounts),
        Instruction::ChangeTicketRecipient(args) => process_change_ticket_recipient(program_id, accounts, args),
    }
}

fn parse_instruction(data: &[u8]) -> Result<Instruction, ProgramError> {
    let (&tag, rest) = data
        .split_first()
        .ok_or(ProgramError::InvalidInstructionData)?;
    let mut reader = Reader::new(rest, ProgramError::InvalidInstructionData);
    let instruction = match tag {
        IX_INIT => Instruction::Init,
        IX_UPDATE_COEF_LIMIT => Instruction::UpdateCoefLimit(UpdateCoefLimitArgs {
            coef_ppm: reader.read_u64()?,
            limit_usd_cents: reader.read_u64()?,
            call_reward_lamports: reader.read_u64()?,
        }),
        IX_GRANT_MANAGER_LIMITS => Instruction::GrantManagerLimits(GrantManagerLimitsArgs {
            manager_wallet: reader.read_pubkey()?,
            add_q1_usd_cents: reader.read_u64()?,
            add_q2_usd_cents: reader.read_u64()?,
            add_q3_usd_cents: reader.read_u64()?,
        }),
        IX_BUY_TICKET => Instruction::BuyTicket(BuyTicketArgs {
            amount_lamports: reader.read_u64()?,
            recipient_wallet: reader.read_pubkey()?,
        }),
        IX_BUY_TICKET_USD => Instruction::BuyTicketUsd(BuyTicketUsdArgs {
            amount_usd_cents: reader.read_u64()?,
            max_pay_lamports: reader.read_u64()?,
            recipient_wallet: reader.read_pubkey()?,
        }),
        IX_BUY_TICKET_SOL => Instruction::BuyTicketSol(BuyTicketSolArgs {
            amount_lamports: reader.read_u64()?,
            min_expected_usd_cents: reader.read_u64()?,
            recipient_wallet: reader.read_pubkey()?,
        }),
        IX_MANAGER_ADD_TICKET => Instruction::ManagerAddTicket(ManagerAddTicketArgs {
            queue_id: reader.read_u8()?,
            recipient_wallet: reader.read_pubkey()?,
            payout_usd_cents: reader.read_u64()?,
        }),
        IX_STEP_PAYOUT => Instruction::StepPayout,
        IX_CHANGE_TICKET_RECIPIENT => Instruction::ChangeTicketRecipient(ChangeTicketRecipientArgs {
            new_recipient_wallet: reader.read_pubkey()?,
        }),
        _ => return Err(ProgramError::InvalidInstructionData),
    };
    reader.finish()?;
    Ok(instruction)
}

fn process_init(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let account_iter = &mut accounts.iter();
    let payer = next_signer_account(account_iter)?;
    let config_pda = next_account_info(account_iter)?;
    let coef_limit_pda = next_account_info(account_iter)?;
    let queues_pda = next_account_info(account_iter)?;
    let inflow_vault_pda = next_account_info(account_iter)?;
    let system_program_ai = next_account_info(account_iter)?;
    require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
    require_system_program(system_program_ai)?;
    ensure_expected_pdas(program_id, config_pda, coef_limit_pda, queues_pda, inflow_vault_pda)?;
    require!(is_uninitialized_account(config_pda), PaymentsError::SystemAlreadyInitialized);
    require!(is_uninitialized_account(coef_limit_pda), PaymentsError::SystemAlreadyInitialized);
    require!(is_uninitialized_account(queues_pda), PaymentsError::SystemAlreadyInitialized);
    require!(is_uninitialized_account(inflow_vault_pda), PaymentsError::SystemAlreadyInitialized);

    let dao_wallet = Pubkey::from_str(settings::DAO_WALLET)
        .map_err(|_| ProgramError::from(PaymentsError::InvalidSettingsWallet))?;

    let config = ConfigState {
        version: 1,
        dao_wallet,
        inflow_vault: *inflow_vault_pda.key,
    };
    create_and_store_state(
        program_id,
        payer,
        system_program_ai,
        config_pda,
        settings::CONFIG_SEED,
        settings::CONFIG_SPACE,
        &config,
    )?;

    let coef_limit = CoefLimitState {
        version: 1,
        coef_ppm: settings::START_COEF_PPM,
        limit_usd_cents: settings::START_LIMIT_USD_CENTS,
        call_reward_lamports: settings::START_CALL_REWARD_LAMPORTS,
    };
    create_and_store_state(
        program_id,
        payer,
        system_program_ai,
        coef_limit_pda,
        settings::COEF_LIMIT_SEED,
        settings::COEF_LIMIT_SPACE,
        &coef_limit,
    )?;

    let queues = QueuesState {
        version: 1,
        q1_tickets_total: 0,
        q1_tickets_paid: 0,
        q1_sum_total_usd_cents: 0,
        q1_sum_paid_usd_cents: 0,
        q2_tickets_total: 0,
        q2_tickets_paid: 0,
        q2_sum_total_usd_cents: 0,
        q2_sum_paid_usd_cents: 0,
        q3_tickets_total: 0,
        q3_tickets_paid: 0,
        q3_sum_total_usd_cents: 0,
        q3_sum_paid_usd_cents: 0,
    };
    create_and_store_state(
        program_id,
        payer,
        system_program_ai,
        queues_pda,
        settings::QUEUES_SEED,
        settings::QUEUES_SPACE,
        &queues,
    )?;

    let vault = VaultState { version: 1 };
    create_and_store_state(
        program_id,
        payer,
        system_program_ai,
        inflow_vault_pda,
        settings::INFLOW_VAULT_SEED,
        settings::INFLOW_VAULT_SPACE,
        &vault,
    )?;

    Ok(())
}

fn process_update_coef_limit(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    args: UpdateCoefLimitArgs,
) -> ProgramResult {
    let account_iter = &mut accounts.iter();
    let signer = next_signer_account(account_iter)?;
    let config_pda = next_account_info(account_iter)?;
    let coef_limit_pda = next_account_info(account_iter)?;
    require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);

    validate_singleton_state_pda(program_id, config_pda, settings::CONFIG_SEED)?;
    validate_singleton_state_pda(program_id, coef_limit_pda, settings::COEF_LIMIT_SEED)?;
    let config = read_state::<ConfigState>(config_pda)?;
    require_keys_eq!(config.dao_wallet, *signer.key, PaymentsError::UnauthorizedDao);
    require!(args.coef_ppm > 0, PaymentsError::InvalidCoefficient);
    require!(args.limit_usd_cents > 0, PaymentsError::InvalidLimit);
    require!(
        args.call_reward_lamports <= settings::MAX_CALL_REWARD_LAMPORTS,
        PaymentsError::InvalidCallReward
    );

    let mut coef_limit = read_state::<CoefLimitState>(coef_limit_pda)?;
    coef_limit.coef_ppm = args.coef_ppm;
    coef_limit.limit_usd_cents = args.limit_usd_cents;
    coef_limit.call_reward_lamports = args.call_reward_lamports;
    write_state(coef_limit_pda, &coef_limit)
}

fn process_grant_manager_limits(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    args: GrantManagerLimitsArgs,
) -> ProgramResult {
    let account_iter = &mut accounts.iter();
    let signer = next_signer_account(account_iter)?;
    let config_pda = next_account_info(account_iter)?;
    let manager_allowance_pda = next_account_info(account_iter)?;
    let system_program_ai = next_account_info(account_iter)?;
    require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
    require_system_program(system_program_ai)?;

    validate_singleton_state_pda(program_id, config_pda, settings::CONFIG_SEED)?;
    let config = read_state::<ConfigState>(config_pda)?;
    require_keys_eq!(config.dao_wallet, *signer.key, PaymentsError::UnauthorizedDao);
    require!(
        args.add_q1_usd_cents > 0 || args.add_q2_usd_cents > 0 || args.add_q3_usd_cents > 0,
        PaymentsError::InvalidAmount
    );

    let (expected_pda, bump) = find_manager_allowance_pda(program_id, &args.manager_wallet);
    require_keys_eq!(expected_pda, *manager_allowance_pda.key, PaymentsError::InvalidPdaAddress);

    let mut state = if is_uninitialized_account(manager_allowance_pda) {
        let initial = ManagerAllowanceState {
            version: 1,
            manager_wallet: args.manager_wallet,
            q1_available_usd_cents: 0,
            q2_available_usd_cents: 0,
            q3_available_usd_cents: 0,
        };
        create_state_with_seeds(
            program_id,
            signer,
            system_program_ai,
            manager_allowance_pda,
            &[
                settings::MANAGER_ALLOWANCE_SEED,
                args.manager_wallet.as_ref(),
                &[bump],
            ],
            settings::MANAGER_ALLOWANCE_SPACE,
            &initial,
        )?;
        initial
    } else {
        read_state::<ManagerAllowanceState>(manager_allowance_pda)?
    };
    require_keys_eq!(state.manager_wallet, args.manager_wallet, PaymentsError::InvalidManagerWallet);

    state.q1_available_usd_cents = checked_add(state.q1_available_usd_cents, args.add_q1_usd_cents)?;
    state.q2_available_usd_cents = checked_add(state.q2_available_usd_cents, args.add_q2_usd_cents)?;
    state.q3_available_usd_cents = checked_add(state.q3_available_usd_cents, args.add_q3_usd_cents)?;
    write_state(manager_allowance_pda, &state)
}

fn process_buy_ticket(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    args: BuyTicketArgs,
) -> ProgramResult {
    let ctx = BuyTicketAccounts::parse(accounts)?;
    let sol_usd = read_sol_usd_price(ctx.sol_usd_price_update, ctx.sol_usd_price_update.key)?;
    let purchase_usd_cents = lamports_to_usd_cents_floor(args.amount_lamports, &sol_usd)?;
    require!(purchase_usd_cents > 0, PaymentsError::InvalidAmount);
    buy_ticket_by_purchase_usd(program_id, &ctx, purchase_usd_cents, args.amount_lamports, args.recipient_wallet)
}

fn process_buy_ticket_usd(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    args: BuyTicketUsdArgs,
) -> ProgramResult {
    let ctx = BuyTicketAccounts::parse(accounts)?;
    require!(args.amount_usd_cents > 0, PaymentsError::InvalidAmount);
    require!(args.max_pay_lamports > 0, PaymentsError::InvalidAmount);
    let sol_usd = read_sol_usd_price(ctx.sol_usd_price_update, ctx.sol_usd_price_update.key)?;
    let pay_lamports = usd_cents_to_lamports_ceil(args.amount_usd_cents, &sol_usd)?;
    require!(pay_lamports > 0, PaymentsError::InvalidAmount);
    require!(pay_lamports <= args.max_pay_lamports, PaymentsError::SlippageExceeded);
    buy_ticket_by_purchase_usd(program_id, &ctx, args.amount_usd_cents, pay_lamports, args.recipient_wallet)
}

fn process_buy_ticket_sol(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    args: BuyTicketSolArgs,
) -> ProgramResult {
    let ctx = BuyTicketAccounts::parse(accounts)?;
    require!(args.amount_lamports > 0, PaymentsError::InvalidAmount);
    let sol_usd = read_sol_usd_price(ctx.sol_usd_price_update, ctx.sol_usd_price_update.key)?;
    let purchase_usd_cents = lamports_to_usd_cents_floor(args.amount_lamports, &sol_usd)?;
    require!(purchase_usd_cents > 0, PaymentsError::InvalidAmount);
    require!(purchase_usd_cents >= args.min_expected_usd_cents, PaymentsError::SlippageExceeded);
    buy_ticket_by_purchase_usd(program_id, &ctx, purchase_usd_cents, args.amount_lamports, args.recipient_wallet)
}

fn process_manager_add_ticket(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    args: ManagerAddTicketArgs,
) -> ProgramResult {
    let account_iter = &mut accounts.iter();
    let signer = next_signer_account(account_iter)?;
    let manager_allowance_pda = next_account_info(account_iter)?;
    let queues_pda = next_account_info(account_iter)?;
    let ticket_pda = next_account_info(account_iter)?;
    let system_program_ai = next_account_info(account_iter)?;
    require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
    require_system_program(system_program_ai)?;

    require!(args.payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount);
    require!(is_valid_queue_id(args.queue_id), PaymentsError::InvalidTicketQueue);
    // Получатель не должен совпадать с inflow-вольтом (см. подробности в
    // buy_ticket_by_purchase_usd): иначе тикет навсегда застрянет в step_payout.
    let (inflow_vault_addr, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED);
    require!(args.recipient_wallet != inflow_vault_addr, PaymentsError::InvalidTicketRecipient);

    let (expected_manager_pda, _) = find_manager_allowance_pda(program_id, signer.key);
    require_keys_eq!(expected_manager_pda, *manager_allowance_pda.key, PaymentsError::InvalidPdaAddress);
    validate_singleton_state_pda(program_id, queues_pda, settings::QUEUES_SEED)?;
    let mut allowance = read_state::<ManagerAllowanceState>(manager_allowance_pda)?;
    require_keys_eq!(allowance.manager_wallet, *signer.key, PaymentsError::InvalidManagerWallet);

    let mut queues = read_state::<QueuesState>(queues_pda)?;
    let debt_before_total = queue_sum_total(&queues, args.queue_id);
    require!(queue_allowance(&allowance, args.queue_id) >= args.payout_usd_cents, PaymentsError::ManagerLimitExceeded);

    let ticket_index = checked_add(queue_total(&queues, args.queue_id), 1)?;
    let (expected_ticket_pda, ticket_bump) = find_ticket_pda(program_id, args.queue_id, ticket_index);
    require_keys_eq!(expected_ticket_pda, *ticket_pda.key, PaymentsError::InvalidPdaAddress);
    require!(is_uninitialized_account(ticket_pda), PaymentsError::PdaAlreadyExists);

    let ticket = TicketState {
        version: 1,
        queue_id: args.queue_id,
        index: ticket_index,
        is_paid: false,
        recipient_wallet: args.recipient_wallet,
        payout_usd_cents: args.payout_usd_cents,
        debt_before_usd_cents: debt_before_total,
    };
    create_state_with_seeds(
        program_id,
        signer,
        system_program_ai,
        ticket_pda,
        &[
            queue_seed(args.queue_id),
            &ticket_index.to_le_bytes(),
            &[ticket_bump],
        ],
        settings::TICKET_SPACE,
        &ticket,
    )?;

    sub_queue_allowance(&mut allowance, args.queue_id, args.payout_usd_cents)?;
    set_queue_total(&mut queues, args.queue_id, ticket_index);
    add_queue_sum_total(&mut queues, args.queue_id, args.payout_usd_cents)?;
    write_state(manager_allowance_pda, &allowance)?;
    write_state(queues_pda, &queues)
}

fn process_step_payout(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let account_iter = &mut accounts.iter();
    let signer = next_signer_account(account_iter)?;
    let config_pda = next_account_info(account_iter)?;
    let queues_pda = next_account_info(account_iter)?;
    let coef_limit_pda = next_account_info(account_iter)?;
    let inflow_vault_pda = next_account_info(account_iter)?;
    let next_ticket_pda = next_account_info(account_iter)?;
    let ticket_recipient_wallet = next_account_info(account_iter)?;
    let dao_wallet = next_account_info(account_iter)?;
    let sol_usd_price_update = next_account_info(account_iter)?;
    require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);

    validate_singleton_state_pda(program_id, config_pda, settings::CONFIG_SEED)?;
    validate_singleton_state_pda(program_id, coef_limit_pda, settings::COEF_LIMIT_SEED)?;
    validate_singleton_state_pda(program_id, queues_pda, settings::QUEUES_SEED)?;
    validate_singleton_state_pda(program_id, inflow_vault_pda, settings::INFLOW_VAULT_SEED)?;
    let config = read_state::<ConfigState>(config_pda)?;
    let coef_limit = read_state::<CoefLimitState>(coef_limit_pda)?;
    let mut queues = read_state::<QueuesState>(queues_pda)?;
    let _vault_state = read_state::<VaultState>(inflow_vault_pda)?;

    require_keys_eq!(*dao_wallet.key, config.dao_wallet, PaymentsError::InvalidDaoWallet);
    require_keys_eq!(*inflow_vault_pda.key, config.inflow_vault, PaymentsError::InvalidInflowVault);

    let q1_pending = checked_sub(queues.q1_tickets_total, queues.q1_tickets_paid)?;
    let q2_pending = checked_sub(queues.q2_tickets_total, queues.q2_tickets_paid)?;
    let q3_pending = checked_sub(queues.q3_tickets_total, queues.q3_tickets_paid)?;

    if q1_pending == 0 && q2_pending == 0 && q3_pending == 0 {
        transfer_all_available_to_dao(inflow_vault_pda, dao_wallet)?;
        return Ok(());
    }

    let (target_queue, next_index) = if q1_pending > 0 {
        (1, checked_add(queues.q1_tickets_paid, 1)?)
    } else if q2_pending > 0 {
        (2, checked_add(queues.q2_tickets_paid, 1)?)
    } else {
        (3, checked_add(queues.q3_tickets_paid, 1)?)
    };

    let (expected_ticket_pda, _) = find_ticket_pda(program_id, target_queue, next_index);
    require_keys_eq!(expected_ticket_pda, *next_ticket_pda.key, PaymentsError::InvalidPdaAddress);

    let mut ticket = read_state::<TicketState>(next_ticket_pda)?;
    require!(ticket.queue_id == target_queue, PaymentsError::InvalidTicketQueue);
    require!(ticket.index == next_index, PaymentsError::InvalidTicketIndex);
    require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid);
    require_keys_eq!(*ticket_recipient_wallet.key, ticket.recipient_wallet, PaymentsError::InvalidTicketRecipient);

    let sol_usd = read_sol_usd_price(sol_usd_price_update, sol_usd_price_update.key)?;
    let ticket_lamports = usd_cents_to_lamports_ceil(ticket.payout_usd_cents, &sol_usd)?;
    let dao_multiplier = target_queue as u64;
    let dao_usd_cents = checked_mul(ticket.payout_usd_cents, dao_multiplier)?;
    let dao_lamports = usd_cents_to_lamports_ceil(dao_usd_cents, &sol_usd)?;
    let needed = checked_add(checked_add(ticket_lamports, dao_lamports)?, coef_limit.call_reward_lamports)?;
    require!(available_vault_lamports(inflow_vault_pda)? >= needed, PaymentsError::NotEnoughInflowForStep);

    transfer_from_vault(inflow_vault_pda, ticket_recipient_wallet, ticket_lamports)?;
    transfer_from_vault(inflow_vault_pda, dao_wallet, dao_lamports)?;
    transfer_from_vault(inflow_vault_pda, signer, coef_limit.call_reward_lamports)?;

    ticket.is_paid = true;
    write_state(next_ticket_pda, &ticket)?;
    mark_queue_paid(&mut queues, target_queue, ticket.payout_usd_cents)?;
    write_state(queues_pda, &queues)
}

fn process_change_ticket_recipient(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    args: ChangeTicketRecipientArgs,
) -> ProgramResult {
    let account_iter = &mut accounts.iter();
    let signer = next_signer_account(account_iter)?;
    let queues_pda = next_account_info(account_iter)?;
    let ticket_pda = next_account_info(account_iter)?;
    require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);

    validate_singleton_state_pda(program_id, queues_pda, settings::QUEUES_SEED)?;
    // Новый получатель не должен совпадать с inflow-вольтом (см. подробности в
    // buy_ticket_by_purchase_usd): иначе тикет навсегда застрянет в step_payout.
    let (inflow_vault_addr, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED);
    require!(args.new_recipient_wallet != inflow_vault_addr, PaymentsError::InvalidTicketRecipient);

    let queues = read_state::<QueuesState>(queues_pda)?;
    let mut ticket = read_state::<TicketState>(ticket_pda)?;
    require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid);
    require_keys_eq!(*signer.key, ticket.recipient_wallet, PaymentsError::UnauthorizedTicketOwner);

    let (expected_ticket_pda, _) = find_ticket_pda(program_id, ticket.queue_id, ticket.index);
    require_keys_eq!(expected_ticket_pda, *ticket_pda.key, PaymentsError::InvalidPdaAddress);

    let q1_pending = checked_sub(queues.q1_tickets_total, queues.q1_tickets_paid)?;
    let q2_pending = checked_sub(queues.q2_tickets_total, queues.q2_tickets_paid)?;
    let q3_pending = checked_sub(queues.q3_tickets_total, queues.q3_tickets_paid)?;

    let next = if q1_pending > 0 {
        Some((1, checked_add(queues.q1_tickets_paid, 1)?))
    } else if q2_pending > 0 {
        Some((2, checked_add(queues.q2_tickets_paid, 1)?))
    } else if q3_pending > 0 {
        Some((3, checked_add(queues.q3_tickets_paid, 1)?))
    } else {
        None
    };

    if let Some((queue_id, next_index)) = next {
        require!(
            !(ticket.queue_id == queue_id && ticket.index == next_index),
            PaymentsError::CannotChangeRecipientForNextPayoutTicket
        );
    }

    ticket.recipient_wallet = args.new_recipient_wallet;
    write_state(ticket_pda, &ticket)
}

struct BuyTicketAccounts<'a, 'info> {
    signer: &'a AccountInfo<'info>,
    config_pda: &'a AccountInfo<'info>,
    coef_limit_pda: &'a AccountInfo<'info>,
    queues_pda: &'a AccountInfo<'info>,
    ticket_pda: &'a AccountInfo<'info>,
    dao_wallet: &'a AccountInfo<'info>,
    sol_usd_price_update: &'a AccountInfo<'info>,
    system_program_ai: &'a AccountInfo<'info>,
}

impl<'a, 'info> BuyTicketAccounts<'a, 'info> {
    fn parse(accounts: &'a [AccountInfo<'info>]) -> Result<Self, ProgramError> {
        let account_iter = &mut accounts.iter();
        let signer = next_signer_account(account_iter)?;
        let config_pda = next_account_info(account_iter)?;
        let coef_limit_pda = next_account_info(account_iter)?;
        let queues_pda = next_account_info(account_iter)?;
        let ticket_pda = next_account_info(account_iter)?;
        let dao_wallet = next_account_info(account_iter)?;
        let sol_usd_price_update = next_account_info(account_iter)?;
        let system_program_ai = next_account_info(account_iter)?;
        require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
        require_system_program(system_program_ai)?;
        Ok(Self {
            signer,
            config_pda,
            coef_limit_pda,
            queues_pda,
            ticket_pda,
            dao_wallet,
            sol_usd_price_update,
            system_program_ai,
        })
    }
}

fn ensure_expected_pdas(
    program_id: &Pubkey,
    config_pda: &AccountInfo,
    coef_limit_pda: &AccountInfo,
    queues_pda: &AccountInfo,
    inflow_vault_pda: &AccountInfo,
) -> ProgramResult {
    let (config, _) = find_single_pda(program_id, settings::CONFIG_SEED);
    let (coef, _) = find_single_pda(program_id, settings::COEF_LIMIT_SEED);
    let (queues, _) = find_single_pda(program_id, settings::QUEUES_SEED);
    let (inflow, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED);
    require_keys_eq!(config, *config_pda.key, PaymentsError::InvalidPdaAddress);
    require_keys_eq!(coef, *coef_limit_pda.key, PaymentsError::InvalidPdaAddress);
    require_keys_eq!(queues, *queues_pda.key, PaymentsError::InvalidPdaAddress);
    require_keys_eq!(inflow, *inflow_vault_pda.key, PaymentsError::InvalidPdaAddress);
    Ok(())
}

fn validate_singleton_state_pda(
    program_id: &Pubkey,
    pda: &AccountInfo,
    seed: &[u8],
) -> ProgramResult {
    let (expected_pda, _) = find_single_pda(program_id, seed);
    require_keys_eq!(expected_pda, *pda.key, PaymentsError::InvalidPdaAddress);
    require_keys_eq!(*pda.owner, id(), PaymentsError::InvalidPdaAddress);
    Ok(())
}

fn find_single_pda(program_id: &Pubkey, seed: &[u8]) -> (Pubkey, u8) {
    Pubkey::find_program_address(&[seed], program_id)
}

fn find_ticket_pda(program_id: &Pubkey, queue_id: u8, index: u64) -> (Pubkey, u8) {
    Pubkey::find_program_address(&[queue_seed(queue_id), &index.to_le_bytes()], program_id)
}

fn find_manager_allowance_pda(program_id: &Pubkey, manager_wallet: &Pubkey) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &[settings::MANAGER_ALLOWANCE_SEED, manager_wallet.as_ref()],
        program_id,
    )
}

fn queue_seed(queue_id: u8) -> &'static [u8] {
    match queue_id {
        1 => settings::Q1_TICKET_SEED,
        2 => settings::Q2_TICKET_SEED,
        3 => settings::Q3_TICKET_SEED,
        _ => settings::Q1_TICKET_SEED,
    }
}

fn is_valid_queue_id(queue_id: u8) -> bool {
    matches!(queue_id, 1 | 2 | 3)
}

fn buy_ticket_by_purchase_usd(
    program_id: &Pubkey,
    ctx: &BuyTicketAccounts<'_, '_>,
    purchase_usd_cents: u64,
    transfer_lamports: u64,
    recipient_wallet: Pubkey,
) -> ProgramResult {
    validate_singleton_state_pda(program_id, ctx.config_pda, settings::CONFIG_SEED)?;
    validate_singleton_state_pda(program_id, ctx.coef_limit_pda, settings::COEF_LIMIT_SEED)?;
    validate_singleton_state_pda(program_id, ctx.queues_pda, settings::QUEUES_SEED)?;
    let config = read_state::<ConfigState>(ctx.config_pda)?;
    let coef_limit = read_state::<CoefLimitState>(ctx.coef_limit_pda)?;
    let mut queues = read_state::<QueuesState>(ctx.queues_pda)?;

    require_keys_eq!(*ctx.dao_wallet.key, config.dao_wallet, PaymentsError::InvalidDaoWallet);
    // Получатель тикета не должен совпадать с inflow-вольтом: иначе в step_payout
    // вольт окажется и источником, и получателем перевода (алиасинг одного аккаунта),
    // второй mutable-займ в transfer_from_vault упадёт, и такой тикет навсегда
    // заморозит обслуживание очереди. Запрещаем такой recipient на входе.
    require!(recipient_wallet != config.inflow_vault, PaymentsError::InvalidTicketRecipient);
    let queue1_sum_total_before = queues.q1_sum_total_usd_cents;
    require!(queue1_sum_total_before < coef_limit.limit_usd_cents, PaymentsError::QueueTemporarilyPaused);

    let ticket_index = checked_add(queues.q1_tickets_total, 1)?;
    let (expected_ticket_pda, ticket_bump) = find_ticket_pda(program_id, 1, ticket_index);
    require_keys_eq!(expected_ticket_pda, *ctx.ticket_pda.key, PaymentsError::InvalidPdaAddress);
    require!(is_uninitialized_account(ctx.ticket_pda), PaymentsError::PdaAlreadyExists);

    let payout_usd_cents = checked_mul(purchase_usd_cents, coef_limit.coef_ppm)? / settings::COEF_SCALE_PPM;
    require!(payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount);

    transfer_from_signer_to_target(ctx.signer, ctx.dao_wallet, ctx.system_program_ai, transfer_lamports)?;

    let ticket = TicketState {
        version: 1,
        queue_id: 1,
        index: ticket_index,
        is_paid: false,
        recipient_wallet,
        payout_usd_cents,
        debt_before_usd_cents: queue1_sum_total_before,
    };
    create_state_with_seeds(
        program_id,
        ctx.signer,
        ctx.system_program_ai,
        ctx.ticket_pda,
        &[
            settings::Q1_TICKET_SEED,
            &ticket_index.to_le_bytes(),
            &[ticket_bump],
        ],
        settings::TICKET_SPACE,
        &ticket,
    )?;

    queues.q1_tickets_total = ticket_index;
    queues.q1_sum_total_usd_cents = checked_add(queues.q1_sum_total_usd_cents, payout_usd_cents)?;
    write_state(ctx.queues_pda, &queues)
}

fn transfer_from_signer_to_target<'info>(
    signer: &AccountInfo<'info>,
    target: &AccountInfo<'info>,
    system_program_ai: &AccountInfo<'info>,
    amount: u64,
) -> ProgramResult {
    let ix = system_instruction::transfer(signer.key, target.key, amount);
    invoke(&ix, &[signer.clone(), target.clone(), system_program_ai.clone()])
}

fn read_sol_usd_price(price_update: &AccountInfo, key: &Pubkey) -> Result<SolUsdPrice, ProgramError> {
    let expected_oracle = Pubkey::from_str(settings::PYTH_SOL_USD_ACCOUNT)
        .map_err(|_| ProgramError::from(PaymentsError::InvalidOracleFeedConfig))?;
    require_keys_eq!(expected_oracle, *key, PaymentsError::InvalidOracleAccount);
    require_keys_eq!(*price_update.owner, pyth_solana_receiver_sdk::ID, PaymentsError::InvalidOracleAccount);

    let data = price_update.try_borrow_data()?;
    let clock = Clock::get()?;
    let mut data_slice: &[u8] = &data;
    let price_update_state = PriceUpdateV2::try_deserialize(&mut data_slice)
        .map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?;
    let feed_id = expected_sol_usd_feed_id()?;
    let price = price_update_state
        .get_price_no_older_than(&clock, settings::ORACLE_MAX_AGE_SECS, &feed_id)
        .map_err(map_pyth_price_error)?;
    require!(price.price > 0, PaymentsError::InvalidOraclePrice);
    require_oracle_confidence_ok(price.price, price.conf)?;
    sol_usd_price_from_components(price.price, price.exponent)
}

fn expected_sol_usd_feed_id() -> Result<FeedId, ProgramError> {
    get_feed_id_from_hex(settings::PYTH_SOL_USD_FEED_ID)
        .map_err(|_| ProgramError::from(PaymentsError::InvalidOracleFeedConfig))
}

fn map_pyth_price_error(err: GetPriceError) -> ProgramError {
    match err {
        GetPriceError::PriceTooOld => PaymentsError::OraclePriceTooOld.into(),
        GetPriceError::MismatchedFeedId => PaymentsError::InvalidOracleFeed.into(),
        GetPriceError::InsufficientVerificationLevel => PaymentsError::InvalidOraclePrice.into(),
        GetPriceError::FeedIdMustBe32Bytes | GetPriceError::FeedIdNonHexCharacter => {
            PaymentsError::InvalidOracleFeedConfig.into()
        }
        GetPriceError::InvalidWindowSize => PaymentsError::InvalidOraclePrice.into(),
    }
}

fn require_oracle_confidence_ok(price: i64, conf: u64) -> ProgramResult {
    require!(price > 0, PaymentsError::InvalidOraclePrice);
    let conf_ppm = checked_mul_u128(conf as u128, 1_000_000u128)? / (price as u128);
    require!(
        conf_ppm <= settings::ORACLE_MAX_CONFIDENCE_PPM as u128,
        PaymentsError::InvalidOraclePrice
    );
    Ok(())
}

fn sol_usd_price_from_components(price: i64, exponent: i32) -> Result<SolUsdPrice, ProgramError> {
    require!(price > 0, PaymentsError::InvalidOraclePrice);
    let mut num = checked_mul_u128(price as u128, settings::USD_CENTS_SCALE as u128)?;
    let mut den: u128 = 1;

    if exponent >= 0 {
        let pow = 10u128
            .checked_pow(exponent as u32)
            .ok_or(ProgramError::from(PaymentsError::MathOverflow))?;
        num = checked_mul_u128(num, pow)?;
    } else {
        let pow = 10u128
            .checked_pow((-exponent) as u32)
            .ok_or(ProgramError::from(PaymentsError::MathOverflow))?;
        den = checked_mul_u128(den, pow)?;
    }

    require!(num > 0 && den > 0, PaymentsError::InvalidOraclePrice);
    Ok(SolUsdPrice { price_num: num, price_den: den })
}

fn lamports_to_usd_cents_floor(lamports: u64, price: &SolUsdPrice) -> Result<u64, ProgramError> {
    let numerator = checked_mul_u128(lamports as u128, price.price_num)?;
    let denominator = checked_mul_u128(settings::LAMPORTS_PER_SOL as u128, price.price_den)?;
    require!(denominator > 0, PaymentsError::InvalidOraclePrice);
    u64::try_from(numerator / denominator).map_err(|_| PaymentsError::MathOverflow.into())
}

fn usd_cents_to_lamports_ceil(usd_cents: u64, price: &SolUsdPrice) -> Result<u64, ProgramError> {
    require!(usd_cents > 0, PaymentsError::InvalidAmount);
    require!(price.price_num > 0, PaymentsError::InvalidOraclePrice);
    let numerator = checked_mul_u128(
        checked_mul_u128(usd_cents as u128, settings::LAMPORTS_PER_SOL as u128)?,
        price.price_den,
    )?;
    let adjusted = numerator
        .checked_add(price.price_num - 1)
        .ok_or(ProgramError::from(PaymentsError::MathOverflow))?;
    u64::try_from(adjusted / price.price_num).map_err(|_| PaymentsError::MathOverflow.into())
}

fn create_and_store_state<'info, T: StateCodec>(
    program_id: &Pubkey,
    payer: &AccountInfo<'info>,
    system_program_ai: &AccountInfo<'info>,
    pda: &AccountInfo<'info>,
    seed: &[u8],
    space: usize,
    state: &T,
) -> ProgramResult {
    let (_, bump) = find_single_pda(program_id, seed);
    create_state_with_seeds(
        program_id,
        payer,
        system_program_ai,
        pda,
        &[seed, &[bump]],
        space,
        state,
    )
}

fn create_state_with_seeds<'info, T: StateCodec>(
    program_id: &Pubkey,
    payer: &AccountInfo<'info>,
    system_program_ai: &AccountInfo<'info>,
    pda: &AccountInfo<'info>,
    seeds: &[&[u8]],
    space: usize,
    state: &T,
) -> ProgramResult {
    create_pda_account(pda, payer, system_program_ai, program_id, seeds, space as u64)?;
    write_state(pda, state)
}

fn create_pda_account<'info>(
    pda: &AccountInfo<'info>,
    payer: &AccountInfo<'info>,
    system_program_ai: &AccountInfo<'info>,
    program_id: &Pubkey,
    seeds: &[&[u8]],
    space: u64,
) -> ProgramResult {
    require!(is_uninitialized_account(pda), PaymentsError::PdaAlreadyExists);
    let required_lamports = Rent::get()?.minimum_balance(space as usize);
    let current_lamports = pda.lamports();

    if current_lamports == 0 {
        // Быстрый путь: адрес пуст — обычное создание аккаунта одной инструкцией.
        let create_ix = system_instruction::create_account(payer.key, pda.key, required_lamports, space, program_id);
        return invoke_signed(
            &create_ix,
            &[payer.clone(), pda.clone(), system_program_ai.clone()],
            &[seeds],
        );
    }

    // На адресе уже лежат лампорты — вероятно, «подсев» атакующим на заранее
    // известный детерминированный адрес тикета/PDA. Создаём «поверх предзаполненного»:
    // доводим ренту переводом, затем allocate + assign под подписью PDA.
    let top_up = required_lamports.saturating_sub(current_lamports);
    if top_up > 0 {
        let transfer_ix = system_instruction::transfer(payer.key, pda.key, top_up);
        invoke(&transfer_ix, &[payer.clone(), pda.clone(), system_program_ai.clone()])?;
    }
    let allocate_ix = system_instruction::allocate(pda.key, space);
    invoke_signed(&allocate_ix, &[pda.clone(), system_program_ai.clone()], &[seeds])?;
    let assign_ix = system_instruction::assign(pda.key, program_id);
    invoke_signed(&assign_ix, &[pda.clone(), system_program_ai.clone()], &[seeds])
}

fn write_state<T: StateCodec>(pda: &AccountInfo, state: &T) -> ProgramResult {
    let bytes = state.encode();
    let mut data = pda.try_borrow_mut_data()?;
    require!(bytes.len() <= data.len(), PaymentsError::InvalidAccountData);
    data.fill(0);
    data[..bytes.len()].copy_from_slice(&bytes);
    Ok(())
}

fn read_state<T: StateCodec>(pda: &AccountInfo) -> Result<T, ProgramError> {
    require!(!is_uninitialized_account(pda), PaymentsError::EmptyState);
    require_keys_eq!(*pda.owner, id(), PaymentsError::InvalidPdaAddress);
    let data = pda.try_borrow_data()?;
    let encoded_len = T::encoded_len();
    require!(data.len() >= encoded_len, PaymentsError::InvalidAccountData);
    T::decode(&data[..encoded_len])
}

// «Инициализируемым» считаем аккаунт без данных, всё ещё принадлежащий System
// Program. Условие lamports() == 0 сознательно убрано: адреса тикетов и прочих
// PDA детерминированы, и любой может заранее перевести на них немного лампортов,
// чтобы заблокировать создание (griefing-DoS). Наличие лампортов больше не должно
// мешать инициализации — за безопасное создание «поверх предзаполненного» отвечает
// create_pda_account. Уже созданный нашей программой PDA имеет данные/владельца id()
// и сюда не пройдёт.
fn is_uninitialized_account(account: &AccountInfo) -> bool {
    account.data_len() == 0
        && (*account.owner == system_program::ID || *account.owner == Pubkey::default())
}

fn available_vault_lamports(vault: &AccountInfo) -> Result<u64, ProgramError> {
    let total = vault.lamports();
    let rent_min = Rent::get()?.minimum_balance(vault.data_len());
    Ok(total.saturating_sub(rent_min))
}

fn transfer_from_vault(vault: &AccountInfo, recipient: &AccountInfo, amount: u64) -> ProgramResult {
    if amount == 0 {
        return Ok(());
    }
    // Защита по умолчанию от алиасинга: источник и получатель не должны быть одним
    // и тем же аккаунтом, иначе второй mutable-займ лампортов ниже завершится ошибкой.
    require!(vault.key != recipient.key, PaymentsError::InvalidTicketRecipient);
    let mut vault_lamports = vault.try_borrow_mut_lamports()?;
    let mut recipient_lamports = recipient.try_borrow_mut_lamports()?;
    require!(**vault_lamports >= amount, PaymentsError::NotEnoughInflowForStep);
    **vault_lamports = checked_sub(**vault_lamports, amount)?;
    **recipient_lamports = checked_add(**recipient_lamports, amount)?;
    Ok(())
}

fn transfer_all_available_to_dao(vault: &AccountInfo, dao_wallet: &AccountInfo) -> ProgramResult {
    let available = available_vault_lamports(vault)?;
    transfer_from_vault(vault, dao_wallet, available)
}

fn next_signer_account<'a, 'info>(
    account_iter: &mut std::slice::Iter<'a, AccountInfo<'info>>,
) -> Result<&'a AccountInfo<'info>, ProgramError> {
    let signer = next_account_info(account_iter)?;
    require!(signer.is_signer, PaymentsError::MissingRequiredSignature);
    Ok(signer)
}

fn require_system_program(system_program_ai: &AccountInfo) -> ProgramResult {
    require_keys_eq!(*system_program_ai.key, system_program::ID, PaymentsError::InvalidSystemProgram);
    Ok(())
}

fn queue_total(queues: &QueuesState, queue_id: u8) -> u64 {
    match queue_id {
        1 => queues.q1_tickets_total,
        2 => queues.q2_tickets_total,
        3 => queues.q3_tickets_total,
        _ => 0,
    }
}

fn queue_sum_total(queues: &QueuesState, queue_id: u8) -> u64 {
    match queue_id {
        1 => queues.q1_sum_total_usd_cents,
        2 => queues.q2_sum_total_usd_cents,
        3 => queues.q3_sum_total_usd_cents,
        _ => 0,
    }
}

fn queue_allowance(allowance: &ManagerAllowanceState, queue_id: u8) -> u64 {
    match queue_id {
        1 => allowance.q1_available_usd_cents,
        2 => allowance.q2_available_usd_cents,
        3 => allowance.q3_available_usd_cents,
        _ => 0,
    }
}

fn sub_queue_allowance(
    allowance: &mut ManagerAllowanceState,
    queue_id: u8,
    amount: u64,
) -> ProgramResult {
    match queue_id {
        1 => allowance.q1_available_usd_cents = checked_sub(allowance.q1_available_usd_cents, amount)?,
        2 => allowance.q2_available_usd_cents = checked_sub(allowance.q2_available_usd_cents, amount)?,
        3 => allowance.q3_available_usd_cents = checked_sub(allowance.q3_available_usd_cents, amount)?,
        _ => return Err(PaymentsError::InvalidTicketQueue.into()),
    }
    Ok(())
}

fn set_queue_total(queues: &mut QueuesState, queue_id: u8, value: u64) {
    match queue_id {
        1 => queues.q1_tickets_total = value,
        2 => queues.q2_tickets_total = value,
        3 => queues.q3_tickets_total = value,
        _ => {}
    }
}

fn add_queue_sum_total(queues: &mut QueuesState, queue_id: u8, value: u64) -> ProgramResult {
    match queue_id {
        1 => queues.q1_sum_total_usd_cents = checked_add(queues.q1_sum_total_usd_cents, value)?,
        2 => queues.q2_sum_total_usd_cents = checked_add(queues.q2_sum_total_usd_cents, value)?,
        3 => queues.q3_sum_total_usd_cents = checked_add(queues.q3_sum_total_usd_cents, value)?,
        _ => return Err(PaymentsError::InvalidTicketQueue.into()),
    }
    Ok(())
}

fn mark_queue_paid(queues: &mut QueuesState, queue_id: u8, payout_usd_cents: u64) -> ProgramResult {
    match queue_id {
        1 => {
            queues.q1_tickets_paid = checked_add(queues.q1_tickets_paid, 1)?;
            queues.q1_sum_paid_usd_cents = checked_add(queues.q1_sum_paid_usd_cents, payout_usd_cents)?;
        }
        2 => {
            queues.q2_tickets_paid = checked_add(queues.q2_tickets_paid, 1)?;
            queues.q2_sum_paid_usd_cents = checked_add(queues.q2_sum_paid_usd_cents, payout_usd_cents)?;
        }
        3 => {
            queues.q3_tickets_paid = checked_add(queues.q3_tickets_paid, 1)?;
            queues.q3_sum_paid_usd_cents = checked_add(queues.q3_sum_paid_usd_cents, payout_usd_cents)?;
        }
        _ => return Err(PaymentsError::InvalidTicketQueue.into()),
    }
    Ok(())
}

fn checked_add(left: u64, right: u64) -> Result<u64, ProgramError> {
    left.checked_add(right).ok_or(PaymentsError::MathOverflow.into())
}

fn checked_sub(left: u64, right: u64) -> Result<u64, ProgramError> {
    left.checked_sub(right).ok_or(PaymentsError::MathOverflow.into())
}

fn checked_mul(left: u64, right: u64) -> Result<u64, ProgramError> {
    left.checked_mul(right).ok_or(PaymentsError::MathOverflow.into())
}

fn checked_mul_u128(left: u128, right: u128) -> Result<u128, ProgramError> {
    left.checked_mul(right).ok_or(PaymentsError::MathOverflow.into())
}
