//! consensus documents - items that vary by consensus flavor
//!
//! **This file is reincluded multiple times**,
//! by the macros in [`crate::doc::ns_variety_definition_macros`],
//! once for votes, and once for each consensus flavour.
//! It is *not* a module `crate::doc::netstatus::rs::each_flavor`.
//!
//! Each time this file is included by one of the macros mentioned above,
//! the `ns_***` macros (such as `ns_const_name!`) may expand to different values.
//!
//! See [`crate::doc::ns_variety_definition_macros`].

use super::*;

ns_use_this_variety! {
    use [crate::doc::netstatus::rs]::?::{RouterStatus};
}
#[cfg(feature = "build_docs")]
ns_use_this_variety! {
    pub(crate) use [crate::doc::netstatus::build]::?::{ConsensusBuilder};
    pub use [crate::doc::netstatus::rs::build]::?::{RouterStatusBuilder};
}

/// A single consensus netstatus, as produced by the old parser.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Consensus {
    /// What kind of consensus document is this?  Absent in votes and
    /// in ns-flavored consensuses.
    pub flavor: ConsensusFlavor,
    /// The preamble, except for the intro item.
    pub preamble: Preamble,
    /// List of voters whose votes contributed to this consensus.
    pub voters: Vec<ConsensusAuthorityEntry>,
    /// A list of routerstatus entries for the relays on the network,
    /// with one entry per relay.
    ///
    /// These are currently ordered by the router's RSA identity, but this is not
    /// to be relied on, since we may want to even abolish RSA at some point!
    pub relays: Vec<RouterStatus>,
    /// Footer for the consensus object.
    pub footer: ConsensusFooterFields,
}

impl Consensus {
    /// Return the Lifetime for this consensus.
    pub fn lifetime(&self) -> &Lifetime {
        &self.preamble.lifetime
    }

    /// Return a slice of all the routerstatus entries in this consensus.
    pub fn relays(&self) -> &[RouterStatus] {
        &self.relays[..]
    }

    /// Return a mapping from keywords to integers representing how
    /// to weight different kinds of relays in different path positions.
    pub fn bandwidth_weights(&self) -> &NetParams<i32> {
        &self.footer.bandwidth_weights
    }

    /// Return the map of network parameters that this consensus advertises.
    pub fn params(&self) -> &NetParams<i32> {
        &self.preamble.params
    }

    /// Return the latest shared random value, if the consensus
    /// contains one.
    pub fn shared_rand_cur(&self) -> Option<&SharedRandStatus> {
        self.preamble.shared_rand.shared_rand_current_value.as_ref()
    }

    /// Return the previous shared random value, if the consensus
    /// contains one.
    pub fn shared_rand_prev(&self) -> Option<&SharedRandStatus> {
        self.preamble.shared_rand.shared_rand_previous_value.as_ref()
    }

    /// Return a [`ProtoStatus`] that lists the network's current requirements and
    /// recommendations for the list of protocols that every relay must implement.  
    pub fn relay_protocol_status(&self) -> &ProtoStatus {
        &self.preamble.proto_statuses.relay
    }

    /// Return a [`ProtoStatus`] that lists the network's current requirements and
    /// recommendations for the list of protocols that every client must implement.
    pub fn client_protocol_status(&self) -> &ProtoStatus {
        &self.preamble.proto_statuses.client
    }

    /// Return a set of all known [`ProtoStatus`] values.
    pub fn protocol_statuses(&self) -> &Arc<ProtoStatuses> {
        &self.preamble.proto_statuses
    }
}

impl Consensus {
    /// Return a new ConsensusBuilder for building test consensus objects.
    ///
    /// This function is only available when the `build_docs` feature has
    /// been enabled.
    #[cfg(feature = "build_docs")]
    pub fn builder() -> ConsensusBuilder {
        ConsensusBuilder::new(RouterStatus::flavor())
    }

    /// Try to parse a single networkstatus document from a string.
    pub fn parse(s: &str) -> crate::Result<(&str, &str, UncheckedConsensus)> {
        let mut reader = NetDocReader::new(s)?;
        Self::parse_from_reader(&mut reader).map_err(|e| e.within(s))
    }
    /// Extract a voter-info section from the reader; return
    /// Ok(None) when we are out of voter-info sections.
    fn take_voterinfo(
        r: &mut NetDocReader<'_, NetstatusKwd>,
    ) -> crate::Result<Option<ConsensusAuthorityEntry>> {
        use NetstatusKwd::*;

        match r.peek() {
            None => return Ok(None),
            Some(e) if e.is_ok_with_kwd_in(&[RS_R, DIRECTORY_FOOTER]) => return Ok(None),
            _ => (),
        };

        let mut first_dir_source = true;
        // TODO: Extract this pattern into a "pause at second"???
        // Pause at the first 'r', or the second 'dir-source'.
        let mut p = r.pause_at(|i| match i {
            Err(_) => false,
            Ok(item) => {
                item.kwd() == RS_R
                    || if item.kwd() == DIR_SOURCE {
                        let was_first = first_dir_source;
                        first_dir_source = false;
                        !was_first
                    } else {
                        false
                    }
            }
        });

        let voter_sec = NS_VOTERINFO_RULES_CONSENSUS.parse(&mut p)?;
        let voter = ConsensusAuthorityEntry::from_section(&voter_sec)?;

        Ok(Some(voter))
    }

    /// Extract the footer (but not signatures) from the reader.
    fn take_footer(r: &mut NetDocReader<'_, NetstatusKwd>) -> crate::Result<ConsensusFooterFields> {
        use NetstatusKwd::*;
        let mut p = r.pause_at(|i| i.is_ok_with_kwd_in(&[DIRECTORY_SIGNATURE]));
        let footer_sec = NS_FOOTER_RULES.parse(&mut p)?;
        let footer = ConsensusFooterFields::from_section(&footer_sec)?;
        Ok(footer)
    }

    /// Extract a routerstatus from the reader.  Return Ok(None) if we're
    /// out of routerstatus entries.
    fn take_routerstatus(r: &mut NetDocReader<'_, NetstatusKwd>) -> crate::Result<Option<(Pos, RouterStatus)>> {
        use NetstatusKwd::*;
        match r.peek() {
            None => return Ok(None),
            Some(e) if e.is_ok_with_kwd_in(&[DIRECTORY_FOOTER]) => return Ok(None),
            _ => (),
        };

        let pos = r.pos();

        let mut first_r = true;
        let mut p = r.pause_at(|i| match i {
            Err(_) => false,
            Ok(item) => {
                item.kwd() == DIRECTORY_FOOTER
                    || if item.kwd() == RS_R {
                        let was_first = first_r;
                        first_r = false;
                        !was_first
                    } else {
                        false
                    }
            }
        });

        let rules = match RouterStatus::flavor() {
            ConsensusFlavor::Microdesc => &NS_ROUTERSTATUS_RULES_MDCON,
            ConsensusFlavor::Plain => &NS_ROUTERSTATUS_RULES_PLAIN,
        };

        let rs_sec = rules.parse(&mut p)?;
        let rs = RouterStatus::from_section(&rs_sec)?;
        Ok(Some((pos, rs)))
    }

    /// Extract an entire UncheckedConsensus from a reader.
    ///
    /// Returns the signed portion of the string, the remainder of the
    /// string, and an UncheckedConsensus.
    fn parse_from_reader<'a>(
        r: &mut NetDocReader<'a, NetstatusKwd>,
    ) -> crate::Result<(&'a str, &'a str, UncheckedConsensus)> {
        use NetstatusKwd::*;
        let ((flavor, preamble), start_pos) = {
            let mut h = r.pause_at(|i| i.is_ok_with_kwd_in(&[DIR_SOURCE]));
            let preamble_sec = NS_HEADER_RULES_CONSENSUS.parse(&mut h)?;
            // Unwrapping should be safe because above `.parse` would have
            // returned an Error
            #[allow(clippy::unwrap_used)]
            let pos = preamble_sec.first_item().unwrap().offset_in(r.str()).unwrap();
            (Preamble::from_section(&preamble_sec)?, pos)
        };
        if RouterStatus::flavor() != flavor {
            return Err(EK::BadDocumentType.with_msg(format!(
                "Expected {:?}, got {:?}",
                RouterStatus::flavor(),
                flavor
            )));
        }

        let mut voters = Vec::new();

        while let Some(voter) = Self::take_voterinfo(r)? {
            voters.push(voter);
        }

        let mut relays: Vec<RouterStatus> = Vec::new();
        while let Some((pos, routerstatus)) = Self::take_routerstatus(r)? {
            if let Some(prev) = relays.last() {
                if prev.rsa_identity() >= routerstatus.rsa_identity() {
                    return Err(EK::WrongSortOrder.at_pos(pos));
                }
            }
            relays.push(routerstatus);
        }
        relays.shrink_to_fit();

        let footer = Self::take_footer(r)?;

        let consensus = Consensus {
            flavor,
            preamble,
            voters,
            relays,
            footer,
        };

        // Find the signatures.
        let mut first_sig: Option<Item<'_, NetstatusKwd>> = None;
        let mut signatures = Vec::new();
        for item in &mut *r {
            let item = item?;
            if item.kwd() != DIRECTORY_SIGNATURE {
                return Err(EK::UnexpectedToken
                    .with_msg(item.kwd().to_str())
                    .at_pos(item.pos()));
            }

            let sig = Signature::from_item(&item)?;
            if first_sig.is_none() {
                first_sig = Some(item);
            }
            signatures.push(sig);
        }

        let end_pos = match first_sig {
            None => return Err(EK::MissingToken.with_msg("directory-signature")),
            // Unwrap should be safe because `first_sig` was parsed from `r`
            #[allow(clippy::unwrap_used)]
            Some(sig) => sig.offset_in(r.str()).unwrap() + "directory-signature ".len(),
        };

        // Find the appropriate digest.
        let signed_str = r.str().get(start_pos..end_pos).ok_or(internal!("chopped utf8"))?;
        let remainder = r.str().get(end_pos..).ok_or(internal!("chopped utf8"))?;
        let (sha256, sha1) = match RouterStatus::flavor() {
            ConsensusFlavor::Plain => (
                None,
                Some(ll::d::Sha1::digest(signed_str.as_bytes()).into()),
            ),
            ConsensusFlavor::Microdesc => (
                Some(ll::d::Sha256::digest(signed_str.as_bytes()).into()),
                None,
            ),
        };
        let hashes = DirectorySignaturesHashesAccu {
            sha256,
            sha1,
            // TODO #2530 This is wrong.  There isn't one hash, there's two.
            sha1_unnamed: sha1,
        };
        let siggroup = SignatureGroup {
            hashes,
            signatures,
        };

        let unval = UnvalidatedConsensus {
            consensus,
            siggroup,
            n_authorities: None,
        };
        let timebound_range = unval.consensus.preamble.validity_time_range();
        let timebound = TimerangeBound::new(unval, timebound_range);
        Ok((signed_str, remainder, timebound))
    }
}

impl Preamble {
    /// Extract the CommonPreamble members from a single preamble section.
    fn from_section(sec: &Section<'_, NetstatusKwd>) -> crate::Result<(ConsensusFlavor, Preamble)> {
        use NetstatusKwd::*;

        {
            // this unwrap is safe because if there is not at least one
            // token in the section, the section is unparsable.
            #[allow(clippy::unwrap_used)]
            let first = sec.first_item().unwrap();
            if first.kwd() != NETWORK_STATUS_VERSION {
                return Err(EK::UnexpectedToken
                    .with_msg(first.kwd().to_str())
                    .at_pos(first.pos()));
            }
        }

        let ver_item = sec.required(NETWORK_STATUS_VERSION)?;

        let version: u32 = ver_item.parse_arg(0)?;
        if version != 3 {
            return Err(EK::BadDocumentVersion.with_msg(version.to_string()));
        }
        let flavor = ConsensusFlavor::from_opt_name(ver_item.arg(1))?;

        let valid_after = sec
            .required(VALID_AFTER)?
            .args_as_str()
            .parse::<Iso8601TimeSp>()?
            .into();
        let fresh_until = sec
            .required(FRESH_UNTIL)?
            .args_as_str()
            .parse::<Iso8601TimeSp>()?
            .into();
        let valid_until = sec
            .required(VALID_UNTIL)?
            .args_as_str()
            .parse::<Iso8601TimeSp>()?
            .into();
        let lifetime = Lifetime::new(valid_after, fresh_until, valid_until)?;

        let parse_rec_versions = |item| {
            let item = sec
                .maybe(item);
            let args = item
                .args_as_str()
                .unwrap_or("")
                // C Tor emits an item with trailing whitespace which we must ignore
                .trim();
            // We want only the first arg, according to the spec.
            // We could want to use MaybeItem::parse_arg, but it treats absence of the
            // argument as an error.  There is no parse_optional_arg on `MaybeItem`.
            // We could add that, but I am trying to avoid adding code to the old parser.
            // So instead we reimplement argument splitting (again).
            args
                .split_once(|c: char| c.is_ascii_whitespace()).map(|(l, _r)| l).unwrap_or(args)
                .parse()
                .map_err(|_e| EK::BadArgument.at_pos(item.pos()))
        };
        let client_versions = parse_rec_versions(CLIENT_VERSIONS)?;
        let server_versions = parse_rec_versions(SERVER_VERSIONS)?;

        let proto_statuses = {
            let client = ProtoStatus::from_section(
                sec,
                RECOMMENDED_CLIENT_PROTOCOLS,
                REQUIRED_CLIENT_PROTOCOLS,
            )?;
            let relay = ProtoStatus::from_section(
                sec,
                RECOMMENDED_RELAY_PROTOCOLS,
                REQUIRED_RELAY_PROTOCOLS,
            )?;
            Arc::new(ProtoStatuses { client, relay })
        };

        let params = sec.maybe(PARAMS).args_as_str().unwrap_or("").parse()?;

        let status: &str = sec.required(VOTE_STATUS)?.arg(0).unwrap_or("");
        if status != "consensus" {
            return Err(EK::BadDocumentType.err());
        }

        // We're ignoring KNOWN_FLAGS in the consensus.

        let consensus_method: u32 = sec.required(CONSENSUS_METHOD)?.parse_arg(0)?;

        let shared_rand_previous_value = sec
            .get(SHARED_RAND_PREVIOUS_VALUE)
            .map(SharedRandStatus::from_item)
            .transpose()?;

        let shared_rand_current_value = sec
            .get(SHARED_RAND_CURRENT_VALUE)
            .map(SharedRandStatus::from_item)
            .transpose()?;

        let voting_delay = if let Some(tok) = sec.get(VOTING_DELAY) {
            let n1 = tok.parse_arg(0)?;
            let n2 = tok.parse_arg(1)?;
            Some((n1, n2))
        } else {
            None
        };

        let shared_rand = SharedRandStatuses {
            shared_rand_previous_value,
            shared_rand_current_value,
            __non_exhaustive: (),
        };

        let preamble = Preamble {
            lifetime,
            client_versions,
            server_versions,
            proto_statuses,
            params,
            voting_delay,
            consensus_method: (consensus_method,),
            published: NotPresent,
            consensus_methods: NotPresent,
            known_flags: DocRelayFlags::new_empty_unknown_discarded(),
            shared_rand,
            __non_exhaustive: (),
        };

        Ok((flavor, preamble))
    }
}

/// A Microdesc consensus whose signatures have not yet been checked.
///
/// To validate this object, call set_n_authorities() on it, then call
/// check_signature() on that result with the set of certs that you
/// have.  Make sure only to provide authority certificates representing
/// real authorities!
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UnvalidatedConsensus {
    /// The consensus object. We don't want to expose this until it's
    /// validated.
    pub consensus: Consensus,
    /// The signatures that need to be validated before we can call
    /// this consensus valid.
    pub siggroup: SignatureGroup,
    /// The total number of authorities that we believe in.  We need
    /// this information in order to validate the signatures, since it
    /// determines how many signatures we need to find valid in `siggroup`.
    pub n_authorities: Option<usize>,
}

impl UnvalidatedConsensus {
    /// Tell the unvalidated consensus how many authorities we believe in.
    ///
    /// Without knowing this number, we can't validate the signature.
    #[must_use]
    pub fn set_n_authorities(self, n_authorities: usize) -> Self {
        UnvalidatedConsensus {
            n_authorities: Some(n_authorities),
            ..self
        }
    }

    /// Return an iterator of all the certificate IDs that we might use
    /// to validate this consensus.
    pub fn signing_cert_ids(&self) -> impl Iterator<Item = AuthCertKeyIds> {
        match self.key_is_correct(&[]) {
            Ok(()) => Vec::new(),
            Err(missing) => missing,
        }
        .into_iter()
    }

    /// Return the lifetime of this unvalidated consensus
    pub fn peek_lifetime(&self) -> &Lifetime {
        self.consensus.lifetime()
    }

    /// Return true if a client who believes in exactly the provided
    /// set of authority IDs might consider this consensus to be
    /// well-signed.
    ///
    /// (This is the case if the consensus claims to be signed by more than
    /// half of the authorities in the list.)
    pub fn authorities_are_correct(&self, authorities: &[&RsaIdentity]) -> bool {
        self.siggroup.could_validate(authorities)
    }

    /// Return the number of relays in this unvalidated consensus.
    ///
    /// This function is unstable. It is only enabled if the crate was
    /// built with the `experimental-api` feature.
    #[cfg(feature = "experimental-api")]
    pub fn n_relays(&self) -> usize {
        self.consensus.relays.len()
    }

    /// Modify the list of relays in this unvalidated consensus.
    ///
    /// A use case for this is long-lasting custom directories. To ensure Arti can still quickly
    /// build circuits when the directory gets old, a tiny churn file can be regularly obtained,
    /// listing no longer available Tor nodes, which can then be removed from the consensus.
    ///
    /// This function is unstable. It is only enabled if the crate was
    /// built with the `experimental-api` feature.
    #[cfg(feature = "experimental-api")]
    pub fn modify_relays<F>(&mut self, func: F)
    where
        F: FnOnce(&mut Vec<RouterStatus>),
    {
        func(&mut self.consensus.relays);
    }
}

impl ExternallySigned<Consensus> for UnvalidatedConsensus {
    type Key = [AuthCert];
    type KeyHint = Vec<AuthCertKeyIds>;
    type Error = Error;

    fn key_is_correct(&self, k: &Self::Key) -> result::Result<(), Self::KeyHint> {
        let (n_ok, missing) = self.siggroup.list_missing(k);
        match self.n_authorities {
            Some(n) if consensus_threshold(n).contains(&n_ok) => Ok(()),
            _ => Err(missing.iter().map(|cert| cert.key_ids).collect()),
        }
    }
    fn is_well_signed(&self, k: &Self::Key) -> result::Result<(), Self::Error> {
        match self.n_authorities {
            None => Err(Error::from(internal!(
                "Didn't set authorities on consensus"
            ))),
            Some(authority) => {
                self.siggroup.validate(authority, k)
                    .map_err(|_: VerifyFailed| EK::BadSignature.err())
            }
        }
    }
    fn dangerously_assume_wellsigned(self) -> Consensus {
        self.consensus
    }
}

/// A Consensus object that has been parsed, but not checked for
/// signatures and timeliness.
pub type UncheckedConsensus = TimerangeBound<UnvalidatedConsensus>;

#[cfg(feature = "incomplete")] // untested
impl NetworkStatusUnverified {
    /// Could we verify this consensus or do we need more authcerts?
    ///
    /// `Ok` means that we have enough authcerts to verify the signature.
    ///
    /// `Err` means that we have not enough authcerts,
    /// or the consensus has not enough signatures.
    /// The [`ConsensusVerifiabilityError`] error gives the details.
    pub fn can_verify(
        &self,
        trusted_authorities: &[RsaIdentity],
        certs_already: &[AuthCert],
    ) -> Result<(), ConsensusVerifiabilityError> {
        let sigs = self.inspect_unverified().1;
        Self::verify_general(
            sigs,
            trusted_authorities,
            certs_already,
            |_signature| {
                // indeed, we don't actuaally verify, so this is a no-op
                Ok(SignatureVerifiedIfIntended {})
            },
        )?;
        Ok(())
    }

    /// Verify the signatures
    ///
    /// Doesn't check the validity period:
    /// the document is wrapped in [`TimerangeBound`],
    /// ensuring that the caller does that check.
    pub fn verify(
        self,
        trusted_authorities: &[RsaIdentity],
        certs: &[AuthCert],
    ) -> Result<TimerangeBound<NetworkStatus>, ConsensusVerifyFailed> {
        let (body, sigs) = self.unwrap_unverified();

        Self::verify_general(
            &sigs,
            trusted_authorities,
            certs,
            |tv| tv.verify().map_err(ConsensusVerifyFailed::InvalidSignature),
        )?;

        let time_range = body.preamble.validity_time_range();
        Ok(TimerangeBound::new(
            body,
            time_range,
        ))
    }

    /// Glue to call `SignatureGroup::verify_general` given our `SignaturesData`
    ///
    /// [`SignatureGroup::verify_general`] contains the actual verification code,
    /// shared between the old parser and the new.
    fn verify_general<E>(
        sigs: &parse2::SignaturesData<Self>,
        trusted: &[RsaIdentity],
        certs: &[AuthCert],
        do_verify: impl Fn(ConsensusSignatureToVerify) -> Result<SignatureVerifiedIfIntended, E>,
    ) -> Result<(), E>
    where ConsensusVerifiabilityError: Into<E>,
    {
        SignatureGroup {
            hashes: sigs.hashes,
            signatures: sigs.sigs.directory_signature.clone(),
        }.verify_general(
            VerifyGeneralTrustedAuthorities::TrustThese { trusted },
            certs,
            do_verify,
        )
    }
}
