use log::debug;
use sp_runtime::SaturatedConversion;
use crate::{
    phron_primitives::BlockNumber,
    data_io::{
        chain_info::ChainInfoProvider,
        legacy::proposal::{PhronProposal, PendingProposalStatus, ProposalStatus},
    },
};
pub fn get_proposal_status<CIP>(
    chain_info_provider: &mut CIP,
    proposal: &PhronProposal,
    old_status: Option<&ProposalStatus>,
) -> ProposalStatus
where
    CIP: ChainInfoProvider,
{
    use PendingProposalStatus::*;
    use ProposalStatus::*;
    let current_highest_finalized = chain_info_provider.get_highest_finalized().number;
    if current_highest_finalized >= proposal.number_top_block() {
        return Ignore;
    }
    if is_hopeless_fork(chain_info_provider, proposal) {
        debug!(target: "phron-finality", "Encountered a hopeless fork proposal {:?}.", proposal);
        return Ignore;
    }
    let old_status = match old_status {
        Some(status) => status,
        None => &Pending(PendingTopBlock),
    };
    match old_status {
        Pending(PendingTopBlock) => {
            let top_block = proposal.top_block();
            if chain_info_provider.is_block_imported(&top_block) {
                if is_branch_ancestry_correct(chain_info_provider, proposal) {
                    if is_ancestor_finalized(chain_info_provider, proposal) {
                        Finalize(
                            proposal
                                .blocks_from_num(current_highest_finalized + 1)
                                .collect(),
                        )
                    } else {
                        Pending(TopBlockImportedButNotFinalizedAncestor)
                    }
                } else {
                    Pending(TopBlockImportedButIncorrectBranch)
                }
            } else {
                Pending(PendingTopBlock)
            }
        }
        Pending(TopBlockImportedButNotFinalizedAncestor) => {
            if is_ancestor_finalized(chain_info_provider, proposal) {
                Finalize(
                    proposal
                        .blocks_from_num(current_highest_finalized + 1)
                        .collect(),
                )
            } else {
                Pending(TopBlockImportedButNotFinalizedAncestor)
            }
        }
        Pending(TopBlockImportedButIncorrectBranch) => {
            Pending(TopBlockImportedButIncorrectBranch)
        }
        _ => old_status.clone(),
    }
}
fn is_hopeless_fork<CIP>(chain_info_provider: &mut CIP, proposal: &PhronProposal) -> bool
where
    CIP: ChainInfoProvider,
{
    let bottom_num = proposal.number_bottom_block();
    for i in 0..proposal.len() {
        if let Ok(finalized_block) =
            chain_info_provider.get_finalized_at(bottom_num + <BlockNumber>::saturated_from(i))
        {
            if finalized_block.hash != proposal[i] {
                return true;
            }
        } else {
            break;
        }
    }
    false
}
fn is_ancestor_finalized<CIP>(chain_info_provider: &mut CIP, proposal: &PhronProposal) -> bool
where
    CIP: ChainInfoProvider,
{
    let bottom = proposal.bottom_block();
    let parent_hash = if let Ok(hash) = chain_info_provider.get_parent_hash(&bottom) {
        hash
    } else {
        return false;
    };
    let finalized =
        if let Ok(hash) = chain_info_provider.get_finalized_at(proposal.number_below_branch()) {
            hash
        } else {
            return false;
        };
    parent_hash == finalized.hash
}
fn is_branch_ancestry_correct<CIP>(chain_info_provider: &mut CIP, proposal: &PhronProposal) -> bool
where
    CIP: ChainInfoProvider,
{
    let bottom_num = proposal.number_bottom_block();
    for i in 1..proposal.len() {
        let curr_num = bottom_num + <BlockNumber>::saturated_from(i);
        let curr_block = proposal.block_at_num(curr_num).expect("is within bounds");
        match chain_info_provider.get_parent_hash(&curr_block) {
            Ok(parent_hash) => {
                if parent_hash != proposal[i - 1] {
                    return false;
                }
            }
            Err(()) => {
                return false;
            }
        }
    }
    true
}
#[cfg(test)]
mod tests {
    use std::{num::NonZeroUsize, sync::Arc};
    use sp_runtime::traits::Block as BlockT;
    use crate::{
        data_io::{
            chain_info::{
                AuxFinalizationChainInfoProvider, CachedChainInfoProvider,
                SubstrateChainInfoProvider,
            },
            proposal::{
                PhronProposal,
                PendingProposalStatus::*,
                ProposalStatus::{self, *},
            },
            status_provider::get_proposal_status,
            ChainInfoCacheConfig, MAX_DATA_BRANCH_LEN,
        },
        testing::{
            client_chain_builder::ClientChainBuilder,
            mocks::{
                unvalidated_proposal_from_headers, TBlock, THeader, TestClient, TestClientBuilder,
                TestClientBuilderExt,
            },
        },
        SessionBoundaryInfo, SessionId, SessionPeriod,
    };
    const DUMMY_SESSION_LEN: u32 = 1_000_000;
    fn proposal_from_headers(headers: Vec<THeader>) -> PhronProposal {
        let unvalidated = unvalidated_proposal_from_headers(headers);
        let session_boundaries = SessionBoundaryInfo::new(SessionPeriod(DUMMY_SESSION_LEN))
            .boundaries_for_session(SessionId(0));
        unvalidated.validate_bounds(&session_boundaries).unwrap()
    }
    fn proposal_from_blocks(blocks: Vec<TBlock>) -> PhronProposal {
        let headers = blocks.into_iter().map(|b| b.header().clone()).collect();
        proposal_from_headers(headers)
    }
    type TestCachedChainInfo =
        CachedChainInfoProvider<SubstrateChainInfoProvider<TBlock, TestClient>>;
    type TestAuxChainInfo =
        AuxFinalizationChainInfoProvider<SubstrateChainInfoProvider<TBlock, TestClient>>;
    fn prepare_proposal_test() -> (ClientChainBuilder, TestCachedChainInfo, TestAuxChainInfo) {
        let client = Arc::new(TestClientBuilder::new().build());
        let config = ChainInfoCacheConfig {
            block_cache_capacity: NonZeroUsize::new(2).unwrap(),
        };
        let cached_chain_info_provider =
            CachedChainInfoProvider::new(SubstrateChainInfoProvider::new(client.clone()), config);
        let chain_builder =
            ClientChainBuilder::new(client.clone(), Arc::new(TestClientBuilder::new().build()));
        let aux_chain_info_provider = AuxFinalizationChainInfoProvider::new(
            SubstrateChainInfoProvider::new(client),
            chain_builder.genesis_hash_num(),
        );
        (
            chain_builder,
            cached_chain_info_provider,
            aux_chain_info_provider,
        )
    }
    fn verify_proposal_status(
        cached_cip: &mut TestCachedChainInfo,
        aux_cip: &mut TestAuxChainInfo,
        proposal: &PhronProposal,
        correct_status: ProposalStatus,
    ) {
        let status_a = get_proposal_status(aux_cip, proposal, None);
        assert_eq!(
            status_a, correct_status,
            "Aux chain info gives wrong status for proposal {proposal:?}"
        );
        let status_c = get_proposal_status(cached_cip, proposal, None);
        assert_eq!(
            status_c, correct_status,
            "Cached chain info gives wrong status for proposal {proposal:?}"
        );
    }
    fn verify_proposal_of_all_lens_finalizable(
        blocks: Vec<TBlock>,
        cached_cip: &mut TestCachedChainInfo,
        aux_cip: &mut TestAuxChainInfo,
    ) {
        for len in 1..=MAX_DATA_BRANCH_LEN {
            let blocks_branch = blocks[0..len].to_vec();
            let proposal = proposal_from_blocks(blocks_branch);
            verify_proposal_status(
                cached_cip,
                aux_cip,
                &proposal,
                ProposalStatus::Finalize(proposal.blocks_from_num(0).collect()),
            );
        }
    }
    #[tokio::test]
    async fn correct_proposals_are_finalizable_even_with_forks() {
        let (mut chain_builder, mut cached_cip, mut aux_cip) = prepare_proposal_test();
        let blocks = chain_builder
            .initialize_single_branch_and_import(MAX_DATA_BRANCH_LEN * 10)
            .await;
        verify_proposal_of_all_lens_finalizable(blocks.clone(), &mut cached_cip, &mut aux_cip);
        let _fork = chain_builder
            .build_and_import_branch_above(&blocks[2].header.hash(), MAX_DATA_BRANCH_LEN * 10)
            .await;
        verify_proposal_of_all_lens_finalizable(blocks.clone(), &mut cached_cip, &mut aux_cip);
    }
    #[tokio::test]
    async fn not_finalized_ancestors_handled_correctly() {
        let (mut chain_builder, mut cached_cip, mut aux_cip) = prepare_proposal_test();
        let blocks = chain_builder
            .initialize_single_branch_and_import(MAX_DATA_BRANCH_LEN * 10)
            .await;
        let fork = chain_builder
            .build_and_import_branch_above(&blocks[2].header.hash(), MAX_DATA_BRANCH_LEN * 10)
            .await;
        for len in 1..=MAX_DATA_BRANCH_LEN {
            let blocks_branch = blocks[1..(len + 1)].to_vec();
            let proposal = proposal_from_blocks(blocks_branch);
            verify_proposal_status(
                &mut cached_cip,
                &mut aux_cip,
                &proposal,
                Pending(TopBlockImportedButNotFinalizedAncestor),
            );
            let blocks_branch = fork[1..(len + 1)].to_vec();
            let proposal = proposal_from_blocks(blocks_branch);
            verify_proposal_status(
                &mut cached_cip,
                &mut aux_cip,
                &proposal,
                Pending(TopBlockImportedButNotFinalizedAncestor),
            );
        }
    }
    #[tokio::test]
    async fn incorrect_branch_handled_correctly() {
        let (mut chain_builder, mut cached_cip, mut aux_cip) = prepare_proposal_test();
        let blocks = chain_builder
            .initialize_single_branch_and_import(MAX_DATA_BRANCH_LEN * 10)
            .await;
        let incorrect_branch = vec![
            blocks[0].clone(),
            blocks[1].clone(),
            blocks[3].clone(),
            blocks[5].clone(),
        ];
        let proposal = proposal_from_blocks(incorrect_branch);
        verify_proposal_status(
            &mut cached_cip,
            &mut aux_cip,
            &proposal,
            Pending(TopBlockImportedButIncorrectBranch),
        );
        chain_builder.finalize_block(&blocks[1].header.hash());
        verify_proposal_status(
            &mut cached_cip,
            &mut aux_cip,
            &proposal,
            Pending(TopBlockImportedButIncorrectBranch),
        );
        chain_builder.finalize_block(&blocks[10].header.hash());
        verify_proposal_status(&mut cached_cip, &mut aux_cip, &proposal, Ignore);
    }
    #[tokio::test]
    async fn pending_top_block_handled_correctly() {
        let (mut chain_builder, mut cached_cip, mut aux_cip) = prepare_proposal_test();
        let blocks = chain_builder
            .initialize_single_branch(MAX_DATA_BRANCH_LEN * 10)
            .await;
        for len in 1..=MAX_DATA_BRANCH_LEN {
            let blocks_branch = blocks[0..len].to_vec();
            let proposal = proposal_from_blocks(blocks_branch);
            verify_proposal_status(
                &mut cached_cip,
                &mut aux_cip,
                &proposal,
                Pending(PendingTopBlock),
            );
        }
        chain_builder.import_branch(blocks.clone()).await;
        verify_proposal_of_all_lens_finalizable(blocks, &mut cached_cip, &mut aux_cip);
    }
    #[tokio::test]
    async fn hopeless_forks_handled_correctly() {
        let (mut chain_builder, mut cached_cip, mut aux_cip) = prepare_proposal_test();
        let blocks = chain_builder
            .initialize_single_branch_and_import(MAX_DATA_BRANCH_LEN * 10)
            .await;
        let fork = chain_builder
            .build_branch_above(&blocks[2].header.hash(), MAX_DATA_BRANCH_LEN * 10)
            .await;
        for len in 1..=MAX_DATA_BRANCH_LEN {
            let fork_branch = fork[0..len].to_vec();
            let proposal = proposal_from_blocks(fork_branch);
            verify_proposal_status(
                &mut cached_cip,
                &mut aux_cip,
                &proposal,
                Pending(PendingTopBlock),
            );
        }
        chain_builder.finalize_block(&blocks[2].header.hash());
        for len in 1..=MAX_DATA_BRANCH_LEN {
            let fork_branch = fork[0..len].to_vec();
            let proposal = proposal_from_blocks(fork_branch);
            verify_proposal_status(
                &mut cached_cip,
                &mut aux_cip,
                &proposal,
                Pending(PendingTopBlock),
            );
        }
        chain_builder.finalize_block(&blocks[3].header.hash());
        for len in 1..=MAX_DATA_BRANCH_LEN {
            let fork_branch = fork[0..len].to_vec();
            let proposal = proposal_from_blocks(fork_branch);
            verify_proposal_status(&mut cached_cip, &mut aux_cip, &proposal, Ignore);
        }
        verify_proposal_status(
            &mut cached_cip,
            &mut aux_cip,
            &proposal_from_blocks(blocks[0..4].to_vec()),
            Ignore,
        );
        let fresh_proposal = proposal_from_blocks(blocks[4..6].to_vec());
        verify_proposal_status(
            &mut cached_cip,
            &mut aux_cip,
            &fresh_proposal,
            Finalize(fresh_proposal.blocks_from_num(0).collect()),
        );
        let long_proposal = proposal_from_blocks(blocks[0..6].to_vec());
        verify_proposal_status(
            &mut cached_cip,
            &mut aux_cip,
            &long_proposal,
            Finalize(fresh_proposal.blocks_from_num(0).collect()),
        );
    }
}