use std::collections::BTreeSet;

use runx_contracts::{Receipt, Reference, ReferenceType};

use super::{ReceiptResolveResult, ReceiptResolver, ReceiptTreeConfig};
use crate::tree::findings::{
    ambiguous_child, join, malformed_child_ref, missing_child, parent_link_findings, resolver_error,
};
use crate::tree::proof::ChildProofPolicy;
use crate::{ReceiptFinding, ReceiptFindingCode};

pub(super) struct TreeTraversal<'a, R: ReceiptResolver, P: ChildProofPolicy> {
    pub(super) resolver: &'a R,
    pub(super) config: ReceiptTreeConfig,
    pub(super) proof_policy: P,
    pub(super) visiting: BTreeSet<String>,
    pub(super) reached: BTreeSet<String>,
}

impl<R: ReceiptResolver, P: ChildProofPolicy> TreeTraversal<'_, R, P> {
    pub(super) fn subtree_findings(
        &mut self,
        path: &str,
        receipt: &Receipt,
        depth: usize,
    ) -> Vec<ReceiptFinding> {
        if !self.visiting.insert(receipt.id.to_string()) {
            return vec![ReceiptFinding {
                code: ReceiptFindingCode::ChildReceiptCycle,
                path: join(path, "id"),
                message: "child receipt refs must not form cycles".to_owned(),
            }];
        }

        let mut findings = Vec::new();
        let empty: Vec<Reference> = Vec::new();
        let child_refs = receipt
            .lineage
            .as_ref()
            .map_or(&empty, |lineage| &lineage.children);
        if child_refs.len() > self.config.max_breadth {
            findings.push(ReceiptFinding {
                code: ReceiptFindingCode::ChildReceiptBreadthLimit,
                path: join(path, "lineage.children"),
                message: "child receipt refs exceed configured breadth limit".to_owned(),
            });
        }

        let child_findings = child_refs
            .iter()
            .take(self.config.max_breadth)
            .enumerate()
            .flat_map(|(index, reference)| {
                self.child_ref_findings(
                    &join(path, &format!("lineage.children[{index}]")),
                    receipt,
                    reference,
                    depth,
                )
            })
            .collect::<Vec<_>>();
        findings.extend(child_findings);
        self.visiting.remove(receipt.id.as_str());
        findings
    }

    fn child_ref_findings(
        &mut self,
        path: &str,
        parent: &Receipt,
        reference: &Reference,
        depth: usize,
    ) -> Vec<ReceiptFinding> {
        if reference.reference_type != ReferenceType::Receipt {
            return vec![malformed_child_ref(path)];
        };
        let next_depth = depth.saturating_add(1);
        if next_depth > self.config.max_depth {
            return vec![ReceiptFinding {
                code: ReceiptFindingCode::ChildReceiptDepthLimit,
                path: path.to_owned(),
                message: "child receipt refs exceed configured depth limit".to_owned(),
            }];
        };
        let resolved = match self.resolver.resolve_child(reference) {
            ReceiptResolveResult::Found(resolved) => resolved,
            ReceiptResolveResult::Missing => return vec![missing_child(path)],
            ReceiptResolveResult::Malformed => return vec![malformed_child_ref(path)],
            ReceiptResolveResult::Ambiguous => return vec![ambiguous_child(path)],
            ReceiptResolveResult::ResolverError => return vec![resolver_error(path)],
        };
        let child = resolved.receipt;
        if self.visiting.contains(child.id.as_str()) {
            return vec![ReceiptFinding {
                code: ReceiptFindingCode::ChildReceiptCycle,
                path: path.to_owned(),
                message: "child receipt refs must not point to an ancestor".to_owned(),
            }];
        }
        let child_path = resolved.path.clone();
        let mut findings = self.proof_policy.findings(&resolved.path, reference, child);
        if self.reached.contains(child.id.as_str()) {
            return findings;
        }
        findings.extend(parent_link_findings(path, parent, child, self.config));
        findings.extend(self.subtree_findings(&child_path, child, next_depth));
        self.reached.insert(child.id.to_string());
        findings
    }
}
