use std::collections::BTreeSet;
use std::io;

use crate::rtr::payload::Payload;
use crate::slurm::policy::{LocallyAddedAssertions, ValidationOutputFilters, prefix_encompasses};

#[derive(Debug, thiserror::Error)]
pub enum SlurmError {
    #[error("failed to parse SLURM JSON: {0}")]
    Parse(#[from] serde_json::Error),

    #[error("invalid SLURM file: {0}")]
    Invalid(String),

    #[error("I/O error while reading SLURM file: {0}")]
    Io(#[from] io::Error),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlurmVersion {
    V1,
    V2,
}

impl SlurmVersion {
    pub const V1_U32: u32 = 1;
    pub const V2_U32: u32 = 2;

    pub fn as_u32(self) -> u32 {
        match self {
            Self::V1 => Self::V1_U32,
            Self::V2 => Self::V2_U32,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlurmFile {
    version: SlurmVersion,
    validation_output_filters: ValidationOutputFilters,
    locally_added_assertions: LocallyAddedAssertions,
}

impl SlurmFile {
    pub fn new(
        version: SlurmVersion,
        validation_output_filters: ValidationOutputFilters,
        locally_added_assertions: LocallyAddedAssertions,
    ) -> Result<Self, SlurmError> {
        let slurm = Self {
            version,
            validation_output_filters,
            locally_added_assertions,
        };
        slurm.validate()?;
        Ok(slurm)
    }

    pub fn version(&self) -> SlurmVersion {
        self.version
    }

    pub fn validation_output_filters(&self) -> &ValidationOutputFilters {
        &self.validation_output_filters
    }

    pub fn locally_added_assertions(&self) -> &LocallyAddedAssertions {
        &self.locally_added_assertions
    }

    pub fn apply(&self, payloads: &[Payload]) -> Vec<Payload> {
        let mut seen = BTreeSet::new();
        let mut result = Vec::new();

        for payload in payloads {
            if self.validation_output_filters.matches(payload) {
                continue;
            }

            if seen.insert(payload.clone()) {
                result.push(payload.clone());
            }
        }

        for assertion in self.locally_added_assertions.to_payloads() {
            if seen.insert(assertion.clone()) {
                result.push(assertion);
            }
        }

        result
    }

    pub fn apply_owned(&self, payloads: Vec<Payload>) -> Vec<Payload> {
        let mut seen = BTreeSet::new();
        let mut result = Vec::new();

        for payload in payloads {
            if self.validation_output_filters.matches(&payload) {
                continue;
            }

            if seen.insert(payload.clone()) {
                result.push(payload);
            }
        }

        for assertion in self.locally_added_assertions.to_payloads() {
            if seen.insert(assertion.clone()) {
                result.push(assertion);
            }
        }

        result
    }

    pub fn merge_named(files: Vec<(String, SlurmFile)>) -> Result<Self, SlurmError> {
        if files.is_empty() {
            return Err(SlurmError::Invalid(
                "SLURM directory does not contain any .slurm files".to_string(),
            ));
        }

        validate_cross_file_conflicts(&files)?;

        let mut version = SlurmVersion::V1;
        let mut merged_filters = ValidationOutputFilters {
            prefix_filters: Vec::new(),
            bgpsec_filters: Vec::new(),
            aspa_filters: Vec::new(),
        };
        let mut merged_assertions = LocallyAddedAssertions {
            prefix_assertions: Vec::new(),
            bgpsec_assertions: Vec::new(),
            aspa_assertions: Vec::new(),
        };

        for (_, file) in files {
            if file.version == SlurmVersion::V2 {
                version = SlurmVersion::V2;
            }

            merged_filters.extend_from(file.validation_output_filters());
            merged_assertions.extend_from(file.locally_added_assertions());
        }

        Self::new(version, merged_filters, merged_assertions)
    }

    fn validate(&self) -> Result<(), SlurmError> {
        self.validation_output_filters.validate(self.version)?;
        self.locally_added_assertions.validate(self.version)?;
        Ok(())
    }
}

fn validate_cross_file_conflicts(files: &[(String, SlurmFile)]) -> Result<(), SlurmError> {
    for i in 0..files.len() {
        for j in (i + 1)..files.len() {
            let (name_a, file_a) = &files[i];
            let (name_b, file_b) = &files[j];

            if prefix_spaces_overlap(file_a, file_b) {
                return Err(SlurmError::Invalid(format!(
                    "conflicting SLURM files: '{}' and '{}' have overlapping prefix spaces",
                    name_a, name_b
                )));
            }

            if let Some(asn) = bgpsec_asn_overlap(file_a, file_b) {
                return Err(SlurmError::Invalid(format!(
                    "conflicting SLURM files: '{}' and '{}' both constrain BGPsec ASN {}",
                    name_a, name_b, asn
                )));
            }

            if let Some(customer_asn) = aspa_customer_overlap(file_a, file_b) {
                return Err(SlurmError::Invalid(format!(
                    "conflicting SLURM files: '{}' and '{}' both constrain ASPA customerAsn {}",
                    name_a, name_b, customer_asn
                )));
            }
        }
    }

    Ok(())
}

fn prefix_spaces_overlap(lhs: &SlurmFile, rhs: &SlurmFile) -> bool {
    let mut lhs_prefixes = lhs
        .validation_output_filters()
        .prefix_filters
        .iter()
        .filter_map(|f| f.prefix.as_ref())
        .chain(
            lhs.locally_added_assertions()
                .prefix_assertions
                .iter()
                .map(|a| &a.prefix),
        );

    let rhs_prefixes = rhs
        .validation_output_filters()
        .prefix_filters
        .iter()
        .filter_map(|f| f.prefix.as_ref())
        .chain(
            rhs.locally_added_assertions()
                .prefix_assertions
                .iter()
                .map(|a| &a.prefix),
        )
        .collect::<Vec<_>>();

    lhs_prefixes.any(|left| {
        rhs_prefixes
            .iter()
            .any(|right| prefix_encompasses(left, right) || prefix_encompasses(right, left))
    })
}

fn bgpsec_asn_overlap(lhs: &SlurmFile, rhs: &SlurmFile) -> Option<u32> {
    let lhs_asns = lhs
        .validation_output_filters()
        .bgpsec_filters
        .iter()
        .filter_map(|f| f.asn)
        .chain(
            lhs.locally_added_assertions()
                .bgpsec_assertions
                .iter()
                .map(|a| a.asn),
        )
        .collect::<BTreeSet<_>>();

    rhs.validation_output_filters()
        .bgpsec_filters
        .iter()
        .filter_map(|f| f.asn)
        .chain(
            rhs.locally_added_assertions()
                .bgpsec_assertions
                .iter()
                .map(|a| a.asn),
        )
        .find_map(|asn| lhs_asns.contains(&asn).then_some(asn.into_u32()))
}

fn aspa_customer_overlap(lhs: &SlurmFile, rhs: &SlurmFile) -> Option<u32> {
    let lhs_customers = lhs
        .validation_output_filters()
        .aspa_filters
        .iter()
        .map(|f| f.customer_asn)
        .chain(
            lhs.locally_added_assertions()
                .aspa_assertions
                .iter()
                .map(|a| a.customer_asn),
        )
        .collect::<BTreeSet<_>>();

    rhs.validation_output_filters()
        .aspa_filters
        .iter()
        .map(|f| f.customer_asn)
        .chain(
            rhs.locally_added_assertions()
                .aspa_assertions
                .iter()
                .map(|a| a.customer_asn),
        )
        .find_map(|asn| lhs_customers.contains(&asn).then_some(asn.into_u32()))
}
