use anchor_lang::prelude::*;

declare_id!("78wgFpYaAnFXJGPLkUCc2xXo9FjwXvkiTCi39EKzovx6");

const MAX_SIGNERS: usize = 10;
const MAX_DESCRIPTION_LEN: usize = 256;
const MAX_PROPOSAL_LIFETIME_SECONDS: i64 = 30 * 24 * 60 * 60;

#[program]
pub mod multisig_governance {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, signers: Vec<Pubkey>, threshold: u8) -> Result<()> {
        let deployment_authorized = false;
        require!(deployment_authorized, GovernanceError::BootstrapDisabled);
        require!(!signers.is_empty(), GovernanceError::NoSigners);
        require!(
            signers.len() <= MAX_SIGNERS,
            GovernanceError::TooManySigners
        );
        require!(
            threshold > 0 && threshold as usize <= signers.len(),
            GovernanceError::InvalidThreshold
        );
        require!(
            !has_duplicate_signers(&signers),
            GovernanceError::DuplicateSigner
        );

        let state = &mut ctx.accounts.multisig_state;
        state.authority = ctx.accounts.authority.key();
        state.signers = signers;
        state.threshold = threshold;
        state.proposal_count = 0;
        state.created_at = Clock::get()?.unix_timestamp;
        Ok(())
    }

    pub fn create_proposal(
        ctx: Context<CreateProposal>,
        description: String,
        expires_at: i64,
    ) -> Result<()> {
        require!(
            description.len() <= MAX_DESCRIPTION_LEN,
            GovernanceError::DescriptionTooLong
        );

        let state = &mut ctx.accounts.multisig_state;
        let proposer = ctx.accounts.proposer.key();
        let now = Clock::get()?.unix_timestamp;
        require!(expires_at > now, GovernanceError::InvalidExpiry);
        require!(
            expires_at
                <= now
                    .checked_add(MAX_PROPOSAL_LIFETIME_SECONDS)
                    .ok_or(GovernanceError::ArithmeticOverflow)?,
            GovernanceError::InvalidExpiry
        );

        // Verify proposer is a signer
        require!(
            state.signers.contains(&proposer),
            GovernanceError::NotASigner
        );

        let proposal_id = state.proposal_count;
        state.proposal_count = state
            .proposal_count
            .checked_add(1)
            .ok_or(GovernanceError::ArithmeticOverflow)?;

        let proposal = &mut ctx.accounts.proposal;
        proposal.id = proposal_id;
        proposal.multisig = state.key();
        proposal.proposer = proposer;
        proposal.description = description.clone();
        proposal.signatures = vec![];
        proposal.finalized = false;
        proposal.created_at = now;
        proposal.expires_at = expires_at;

        emit!(ProposalCreated {
            proposal_id,
            proposer,
            description,
            created_at: proposal.created_at,
        });
        Ok(())
    }

    pub fn sign_proposal(ctx: Context<SignProposal>) -> Result<()> {
        let state = &ctx.accounts.multisig_state;
        let signer = ctx.accounts.signer.key();

        require!(state.signers.contains(&signer), GovernanceError::NotASigner);

        let proposal = &mut ctx.accounts.proposal;
        require!(!proposal.finalized, GovernanceError::AlreadyFinalized);
        require!(
            Clock::get()?.unix_timestamp <= proposal.expires_at,
            GovernanceError::ProposalExpired
        );
        require!(
            !proposal.signatures.contains(&signer),
            GovernanceError::AlreadySigned
        );

        proposal.signatures.push(signer);

        emit!(ProposalSigned {
            proposal_id: proposal.id,
            signer,
            total_signatures: proposal.signatures.len() as u8,
        });
        Ok(())
    }

    /// Finalization records that a bounded, off-chain governance proposal passed.
    /// It intentionally performs no CPI or privileged action. A future action
    /// dispatcher must be separately specified and reviewed rather than turning
    /// an arbitrary account list into an executable governance payload.
    pub fn finalize_proposal(ctx: Context<FinalizeProposal>) -> Result<()> {
        let state = &ctx.accounts.multisig_state;
        let proposal = &mut ctx.accounts.proposal;

        require!(!proposal.finalized, GovernanceError::AlreadyFinalized);
        require!(
            Clock::get()?.unix_timestamp <= proposal.expires_at,
            GovernanceError::ProposalExpired
        );
        require!(
            state.signers.contains(&ctx.accounts.executor.key()),
            GovernanceError::NotASigner
        );
        require!(
            proposal.signatures.len() >= state.threshold as usize,
            GovernanceError::ThresholdNotMet
        );

        proposal.finalized = true;

        emit!(ProposalFinalized {
            proposal_id: proposal.id,
            executed_at: Clock::get()?.unix_timestamp,
            total_signatures: proposal.signatures.len() as u8,
        });
        Ok(())
    }

    /// Anyone may reclaim an expired proposal's rent to its recorded proposer.
    /// The refund destination is address-constrained and cannot be redirected.
    pub fn close_expired_proposal(ctx: Context<CloseExpiredProposal>) -> Result<()> {
        require!(
            Clock::get()?.unix_timestamp > ctx.accounts.proposal.expires_at,
            GovernanceError::ProposalNotExpired
        );
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = authority,
        space = 8 + 32 + 4 + (32 * MAX_SIGNERS) + 1 + 8 + 8,
        seeds = [b"multisig"],
        bump
    )]
    pub multisig_state: Account<'info, MultisigState>,
    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct CreateProposal<'info> {
    #[account(
        mut,
        seeds = [b"multisig"],
        bump
    )]
    pub multisig_state: Account<'info, MultisigState>,
    #[account(
        init,
        payer = proposer,
        space = 8 + 8 + 32 + 32 + 4 + MAX_DESCRIPTION_LEN + 4 + (32 * MAX_SIGNERS) + 1 + 8 + 8,
        seeds = [b"proposal", multisig_state.key().as_ref(), multisig_state.proposal_count.to_le_bytes().as_ref()],
        bump
    )]
    pub proposal: Account<'info, Proposal>,
    #[account(mut)]
    pub proposer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct SignProposal<'info> {
    #[account(
        seeds = [b"multisig"],
        bump
    )]
    pub multisig_state: Account<'info, MultisigState>,
    #[account(
        mut,
        seeds = [b"proposal", multisig_state.key().as_ref(), proposal.id.to_le_bytes().as_ref()],
        bump,
        constraint = proposal.multisig == multisig_state.key() @ GovernanceError::InvalidProposal
    )]
    pub proposal: Account<'info, Proposal>,
    pub signer: Signer<'info>,
}

#[derive(Accounts)]
pub struct FinalizeProposal<'info> {
    #[account(
        seeds = [b"multisig"],
        bump
    )]
    pub multisig_state: Account<'info, MultisigState>,
    #[account(
        mut,
        close = proposer,
        seeds = [b"proposal", multisig_state.key().as_ref(), proposal.id.to_le_bytes().as_ref()],
        bump,
        constraint = proposal.multisig == multisig_state.key() @ GovernanceError::InvalidProposal
    )]
    pub proposal: Account<'info, Proposal>,
    pub executor: Signer<'info>,
    #[account(mut, address = proposal.proposer @ GovernanceError::InvalidProposer)]
    pub proposer: SystemAccount<'info>,
}

#[derive(Accounts)]
pub struct CloseExpiredProposal<'info> {
    #[account(
        seeds = [b"multisig"],
        bump
    )]
    pub multisig_state: Account<'info, MultisigState>,
    #[account(
        mut,
        close = proposer,
        seeds = [b"proposal", multisig_state.key().as_ref(), proposal.id.to_le_bytes().as_ref()],
        bump,
        constraint = proposal.multisig == multisig_state.key() @ GovernanceError::InvalidProposal
    )]
    pub proposal: Account<'info, Proposal>,
    #[account(mut, address = proposal.proposer @ GovernanceError::InvalidProposer)]
    pub proposer: SystemAccount<'info>,
}

#[account]
pub struct MultisigState {
    pub authority: Pubkey,
    pub signers: Vec<Pubkey>,
    pub threshold: u8,
    pub proposal_count: u64,
    pub created_at: i64,
}

#[account]
pub struct Proposal {
    pub id: u64,
    pub multisig: Pubkey,
    pub proposer: Pubkey,
    pub description: String,
    pub signatures: Vec<Pubkey>,
    pub finalized: bool,
    pub created_at: i64,
    pub expires_at: i64,
}

#[event]
pub struct ProposalCreated {
    pub proposal_id: u64,
    pub proposer: Pubkey,
    pub description: String,
    pub created_at: i64,
}

#[event]
pub struct ProposalSigned {
    pub proposal_id: u64,
    pub signer: Pubkey,
    pub total_signatures: u8,
}

#[event]
pub struct ProposalFinalized {
    pub proposal_id: u64,
    pub executed_at: i64,
    pub total_signatures: u8,
}

#[error_code]
pub enum GovernanceError {
    #[msg("No signers provided")]
    NoSigners,
    #[msg("Too many signers (max 10)")]
    TooManySigners,
    #[msg("Invalid threshold")]
    InvalidThreshold,
    #[msg("Not a registered signer")]
    NotASigner,
    #[msg("Proposal already finalized")]
    AlreadyFinalized,
    #[msg("Signer has already signed this proposal")]
    AlreadySigned,
    #[msg("Threshold not met for execution")]
    ThresholdNotMet,
    #[msg("Description exceeds maximum length")]
    DescriptionTooLong,
    #[msg("Duplicate signer")]
    DuplicateSigner,
    #[msg("Proposal expiry must be in the bounded future")]
    InvalidExpiry,
    #[msg("Proposal has expired")]
    ProposalExpired,
    #[msg("Proposal has not expired")]
    ProposalNotExpired,
    #[msg("Proposal is not linked to this multisig state")]
    InvalidProposal,
    #[msg("Proposal rent refund recipient is not the recorded proposer")]
    InvalidProposer,
    #[msg("Arithmetic overflow")]
    ArithmeticOverflow,
    #[msg("Bootstrap is disabled until a source-backed partner authority design is approved")]
    BootstrapDisabled,
}

fn has_duplicate_signers(signers: &[Pubkey]) -> bool {
    signers
        .iter()
        .enumerate()
        .any(|(index, signer)| signers[..index].contains(signer))
}
