//! clud — an unframeworked solana program.
//! the state only rises, the record only lengthens, the weather only comes at a record.

use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    instruction::{AccountMeta, Instruction},
    program::{invoke, invoke_signed},
    program_error::ProgramError,
    pubkey::Pubkey,
    rent::Rent,
    system_instruction,
    sysvar::Sysvar,
};

entrypoint!(process_instruction);

const STATE_SEED: &[u8] = b"clud";
const STAKE_SEED: &[u8] = b"stake";
const POOL_SEED: &[u8] = b"pool";

const SLOT_HASHES_ID: Pubkey = solana_program::pubkey!("SysvarS1otHashes111111111111111111111111111");
const TOKEN_PROGRAM_ID: Pubkey = solana_program::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");

/// The observer's cut of the vault on a rain, in basis points.
const OBSERVER_BPS: u64 = 200; // 2%

/// SlotHashes entry: u64 slot + 32-byte hash.
const SH_ENTRY: usize = 40;
/// SlotHashes header: u64 length.
const SH_HEADER: usize = 8;

const STATE_LEN: usize = 88;
const STAKE_LEN: usize = 34;

struct State<'a> { data: &'a mut [u8] }

impl<'a> State<'a> {
    fn load(a: &'a mut [u8]) -> Result<Self, ProgramError> {
        if a.len() != STATE_LEN || a[0] != 1 {
            return Err(ProgramError::InvalidAccountData);
        }
        Ok(Self { data: a })
    }
    fn level(&self) -> u16 { u16::from_le_bytes(self.data[2..4].try_into().unwrap()) }
    fn set_level(&mut self, v: u16) { self.data[2..4].copy_from_slice(&v.to_le_bytes()) }
    fn rains(&self) -> u32 { u32::from_le_bytes(self.data[4..8].try_into().unwrap()) }
    fn set_rains(&mut self, v: u32) { self.data[4..8].copy_from_slice(&v.to_le_bytes()) }
    fn observations(&self) -> u64 { u64::from_le_bytes(self.data[8..16].try_into().unwrap()) }
    fn set_observations(&mut self, v: u64) { self.data[8..16].copy_from_slice(&v.to_le_bytes()) }
    fn last_slot(&self) -> u64 { u64::from_le_bytes(self.data[16..24].try_into().unwrap()) }
    fn set_last_slot(&mut self, v: u64) { self.data[16..24].copy_from_slice(&v.to_le_bytes()) }
    fn unseen(&self) -> u64 { u64::from_le_bytes(self.data[24..32].try_into().unwrap()) }
    fn set_unseen(&mut self, v: u64) { self.data[24..32].copy_from_slice(&v.to_le_bytes()) }
    fn total_staked(&self) -> u64 { u64::from_le_bytes(self.data[32..40].try_into().unwrap()) }
    fn set_total_staked(&mut self, v: u64) { self.data[32..40].copy_from_slice(&v.to_le_bytes()) }
    fn reward_index(&self) -> u128 { u128::from_le_bytes(self.data[40..56].try_into().unwrap()) }
    fn set_reward_index(&mut self, v: u128) { self.data[40..56].copy_from_slice(&v.to_le_bytes()) }
}

struct StakePos<'a> { data: &'a mut [u8] }

impl<'a> StakePos<'a> {
    fn load(a: &'a mut [u8]) -> Result<Self, ProgramError> {
        if a.len() != STAKE_LEN || a[0] != 1 {
            return Err(ProgramError::InvalidAccountData);
        }
        Ok(Self { data: a })
    }
    fn amount(&self) -> u64 { u64::from_le_bytes(self.data[2..10].try_into().unwrap()) }
    fn set_amount(&mut self, v: u64) { self.data[2..10].copy_from_slice(&v.to_le_bytes()) }
    fn reward_debt(&self) -> u128 { u128::from_le_bytes(self.data[10..26].try_into().unwrap()) }
    fn set_reward_debt(&mut self, v: u128) { self.data[10..26].copy_from_slice(&v.to_le_bytes()) }
    fn pending(&self) -> u64 { u64::from_le_bytes(self.data[26..34].try_into().unwrap()) }
    fn set_pending(&mut self, v: u64) { self.data[26..34].copy_from_slice(&v.to_le_bytes()) }
}

fn process_instruction(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    match data.first() {
        Some(0) => init(program_id, accounts),
        Some(1) => observe(program_id, accounts),
        Some(2) => stake(program_id, accounts, &data[1..]),
        Some(3) => unstake(program_id, accounts, &data[1..]),
        Some(4) => claim(program_id, accounts),
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

/// Callable once. Creates the state account and records the mint.
fn init(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let payer = &accounts[0];
    let state_ai = &accounts[1];
    let mint = &accounts[2];
    let system = &accounts[4];

    if !payer.is_signer { return Err(ProgramError::MissingRequiredSignature); }

    let (state_key, bump) = Pubkey::find_program_address(&[STATE_SEED], program_id);
    if state_key != *state_ai.key { return Err(ProgramError::InvalidSeeds); }
    if !state_ai.data_is_empty() { return Err(ProgramError::AccountAlreadyInitialized); }

    let rent = Rent::get()?;
    invoke_signed(
        &system_instruction::create_account(
            payer.key, state_ai.key, rent.minimum_balance(STATE_LEN),
            STATE_LEN as u64, program_id,
        ),
        &[payer.clone(), state_ai.clone(), system.clone()],
        &[&[STATE_SEED, &[bump]]],
    )?;

    let mut d = state_ai.try_borrow_mut_data()?;
    d[0] = 1;
    d[1] = bump;
    d[56..88].copy_from_slice(mint.key.as_ref());
    Ok(())
}

/// Permissionless. Counts the holes in the current view and rains at a record.
fn observe(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let state_ai = &accounts[0];
    let sh = &accounts[1];
    let caller = &accounts[2];

    if state_ai.owner != program_id { return Err(ProgramError::IllegalOwner); }
    if *sh.key != SLOT_HASHES_ID { return Err(ProgramError::InvalidArgument); }
    if !caller.is_signer { return Err(ProgramError::MissingRequiredSignature); }

    // The sysvar is a u64 length then (u64 slot, [u8;32]) entries, newest first.
    // Three fixed offsets. The entries are never walked.
    let shd = sh.try_borrow_data()?;
    if shd.len() < SH_HEADER + SH_ENTRY { return Err(ProgramError::InvalidAccountData); }
    let len = u64::from_le_bytes(shd[0..8].try_into().unwrap()) as usize;
    if len < 2 || shd.len() < SH_HEADER + len * SH_ENTRY {
        return Err(ProgramError::InvalidAccountData);
    }
    let newest = u64::from_le_bytes(shd[SH_HEADER..SH_HEADER + 8].try_into().unwrap());
    let last_off = SH_HEADER + (len - 1) * SH_ENTRY;
    let oldest = u64::from_le_bytes(shd[last_off..last_off + 8].try_into().unwrap());
    drop(shd);

    // Slots spanned minus entries present is the count of blocks never produced.
    let span = newest.checked_sub(oldest).and_then(|d| d.checked_add(1))
        .ok_or(ProgramError::ArithmeticOverflow)?;
    let gaps = span.saturating_sub(len as u64);
    let gaps_u16 = u16::try_from(gaps.min(u16::MAX as u64)).unwrap();

    let mut sd = state_ai.try_borrow_mut_data()?;
    let mut st = State::load(&mut sd)?;

    // Weather that happened while nobody was looking. Recorded, never estimated.
    let last = st.last_slot();
    if last != 0 && last + 1 < oldest {
        st.set_unseen(st.unseen().saturating_add(oldest - last - 1));
    }

    if gaps_u16 > st.level() {
        let rent = Rent::get()?;
        let floor = rent.minimum_balance(STATE_LEN);
        let vapor = state_ai.lamports().saturating_sub(floor);
        if vapor > 0 {
            let to_caller = vapor * OBSERVER_BPS / 10_000;
            let to_pool = vapor - to_caller;
            **state_ai.try_borrow_mut_lamports()? -= to_caller;
            **caller.try_borrow_mut_lamports()? += to_caller;
            let staked = st.total_staked();
            if staked > 0 {
                let delta = ((to_pool as u128) << 64) / staked as u128;
                st.set_reward_index(st.reward_index().wrapping_add(delta));
            }
            // If nothing is staked the pool share stays in the vault and joins
            // the next rain. Nothing is stranded and nothing is kept.
        }
        st.set_level(gaps_u16);
        st.set_rains(st.rains() + 1);
    }

    st.set_last_slot(newest);
    st.set_observations(st.observations() + 1);
    Ok(())
}

fn settle(st: &State, pos: &mut StakePos) {
    let owed = ((st.reward_index().wrapping_sub(pos.reward_debt()))
        .saturating_mul(pos.amount() as u128)) >> 64;
    pos.set_pending(pos.pending().saturating_add(owed as u64));
    pos.set_reward_debt(st.reward_index());
}

fn token_transfer<'a>(
    from: &AccountInfo<'a>, to: &AccountInfo<'a>, auth: &AccountInfo<'a>,
    token_prog: &AccountInfo<'a>, amount: u64, signer_seeds: Option<&[&[u8]]>,
) -> ProgramResult {
    let mut data = Vec::with_capacity(9);
    data.push(3u8);
    data.extend_from_slice(&amount.to_le_bytes());
    let ix = Instruction {
        program_id: TOKEN_PROGRAM_ID,
        accounts: vec![
            AccountMeta::new(*from.key, false),
            AccountMeta::new(*to.key, false),
            AccountMeta::new_readonly(*auth.key, signer_seeds.is_none()),
        ],
        data,
    };
    let infos = [from.clone(), to.clone(), auth.clone(), token_prog.clone()];
    match signer_seeds {
        Some(seeds) => invoke_signed(&ix, &infos, &[seeds]),
        None => invoke(&ix, &infos),
    }
}

fn stake_pda(program_id: &Pubkey, user: &Pubkey) -> (Pubkey, u8) {
    Pubkey::find_program_address(&[STAKE_SEED, user.as_ref()], program_id)
}

/// Coin in. No lockup begins, because there is none.
fn stake(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let amount = u64::from_le_bytes(
        data.get(0..8).ok_or(ProgramError::InvalidInstructionData)?.try_into().unwrap());
    if amount == 0 { return Err(ProgramError::InvalidInstructionData); }

    let state_ai = &accounts[0];
    let user = &accounts[1];
    let pos_ai = &accounts[2];
    let user_tok = &accounts[3];
    let pool_tok = &accounts[4];
    let token_prog = &accounts[5];
    let system = &accounts[6];

    if state_ai.owner != program_id { return Err(ProgramError::IllegalOwner); }
    if !user.is_signer { return Err(ProgramError::MissingRequiredSignature); }

    let (pos_key, pos_bump) = stake_pda(program_id, user.key);
    if pos_key != *pos_ai.key { return Err(ProgramError::InvalidSeeds); }
    let (pool_key, _) = Pubkey::find_program_address(&[POOL_SEED], program_id);
    if pool_key != *pool_tok.key { return Err(ProgramError::InvalidSeeds); }

    if pos_ai.data_is_empty() {
        let rent = Rent::get()?;
        invoke_signed(
            &system_instruction::create_account(
                user.key, pos_ai.key, rent.minimum_balance(STAKE_LEN),
                STAKE_LEN as u64, program_id,
            ),
            &[user.clone(), pos_ai.clone(), system.clone()],
            &[&[STAKE_SEED, user.key.as_ref(), &[pos_bump]]],
        )?;
        let mut pd = pos_ai.try_borrow_mut_data()?;
        pd[0] = 1;
        pd[1] = pos_bump;
    }

    token_transfer(user_tok, pool_tok, user, token_prog, amount, None)?;

    let mut sd = state_ai.try_borrow_mut_data()?;
    let mut st = State::load(&mut sd)?;
    let mut pd = pos_ai.try_borrow_mut_data()?;
    let mut pos = StakePos::load(&mut pd)?;

    settle(&st, &mut pos);
    pos.set_amount(pos.amount().checked_add(amount).ok_or(ProgramError::ArithmeticOverflow)?);
    st.set_total_staked(st.total_staked().checked_add(amount).ok_or(ProgramError::ArithmeticOverflow)?);
    Ok(())
}

/// Coin out. No queue, no penalty, no waiting.
fn unstake(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let amount = u64::from_le_bytes(
        data.get(0..8).ok_or(ProgramError::InvalidInstructionData)?.try_into().unwrap());

    let state_ai = &accounts[0];
    let user = &accounts[1];
    let pos_ai = &accounts[2];
    let user_tok = &accounts[3];
    let pool_tok = &accounts[4];
    let token_prog = &accounts[5];

    if state_ai.owner != program_id || pos_ai.owner != program_id {
        return Err(ProgramError::IllegalOwner);
    }
    if !user.is_signer { return Err(ProgramError::MissingRequiredSignature); }

    let (pos_key, _) = stake_pda(program_id, user.key);
    if pos_key != *pos_ai.key { return Err(ProgramError::InvalidSeeds); }

    let mut sd = state_ai.try_borrow_mut_data()?;
    let mut st = State::load(&mut sd)?;
    let mut pd = pos_ai.try_borrow_mut_data()?;
    let mut pos = StakePos::load(&mut pd)?;

    settle(&st, &mut pos);
    if amount > pos.amount() { return Err(ProgramError::InsufficientFunds); }
    pos.set_amount(pos.amount() - amount);
    st.set_total_staked(st.total_staked() - amount);
    drop(pd);
    drop(sd);

    let bump = { let sd = state_ai.try_borrow_data()?; sd[1] };
    token_transfer(pool_tok, user_tok, state_ai, token_prog, amount,
        Some(&[STATE_SEED, &[bump]]))
}

/// Accrued lamports, pro rata, constant time regardless of dry observations sat through.
fn claim(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let state_ai = &accounts[0];
    let user = &accounts[1];
    let pos_ai = &accounts[2];

    if state_ai.owner != program_id || pos_ai.owner != program_id {
        return Err(ProgramError::IllegalOwner);
    }
    if !user.is_signer { return Err(ProgramError::MissingRequiredSignature); }

    let (pos_key, _) = stake_pda(program_id, user.key);
    if pos_key != *pos_ai.key { return Err(ProgramError::InvalidSeeds); }

    let mut sd = state_ai.try_borrow_mut_data()?;
    let st = State::load(&mut sd)?;
    let mut pd = pos_ai.try_borrow_mut_data()?;
    let mut pos = StakePos::load(&mut pd)?;

    settle(&st, &mut pos);
    let owed = pos.pending();
    if owed == 0 { return Ok(()); }
    pos.set_pending(0);
    drop(pd);
    drop(sd);

    **state_ai.try_borrow_mut_lamports()? -= owed;
    **user.try_borrow_mut_lamports()? += owed;
    Ok(())
}