//! Figure profiles (`extensions/energy-measurement.md` §6.1): how a report about one
//! energy figure — a `measurement` or `operating-point` claim (jwp-receipts §2.4.8–§2.4.9)
//! — names the receipt that seals the figure, and what it reads there.
//!
//! A profile is named by the domain its receipt's identity is computed under, without
//! the domain's final version byte, so the registry cannot drift from the code: a
//! receipt family's identity domain *is* its profile's name. A profile fixes the
//! claim's subject type, how the identity is computed, which figure an index selects,
//! where the figure's class is, and which key signed it.
//!
//! [`EnergyFigures::figure_view`] gives, for a receipt this crate has verified, what a
//! report is checked against at jwp-receipts §3.1 step 6: the profile, the identity,
//! the figures with their classes, and the Ed25519 key that signed them. It is `None`
//! for a receipt that does not verify. No receipt family is changed by being viewed.
//!
//! The quantum receipts are viewed through `wai-quantum`'s `MaybeLabelled` and
//! `LabelledQbom`. Their profile names are read from the receipt type's identity
//! domain through the crate's labelling trait, whose methods `wai-quantum` documents
//! as plumbing rather than a public API; a test pins each type's profile name, so a
//! change there fails loudly here.

use crate::claim_receipt::{jcs, ClaimBody, ClaimReceipt, SubjectType};
use crate::energy_class::EnergyProvenance;

/// One figure profile of energy-measurement §6.1.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Profile {
    pub name: &'static str,
    /// The `subject_type` a claim naming this profile carries.
    pub subject_type: SubjectType,
    /// Whether the receipt carries several figures, selected by `figure_index`.
    pub indexed: bool,
    /// Whether an Ed25519 key signs the figure. Only that key may sign a
    /// `measurement` claim about it.
    pub ed25519_signer: bool,
    /// Whether the figure is named within a group: the `moq-jwp:object` profile, whose
    /// subject is a content hash that more than one group can deliver.
    pub grouped: bool,
}

const fn p(name: &'static str, subject_type: SubjectType, indexed: bool, ed25519_signer: bool, grouped: bool) -> Profile {
    Profile { name, subject_type, indexed, ed25519_signer, grouped }
}

const PROFILES: &[Profile] = &[
    p("moq-jwp:object", SubjectType::Object, true, true, true),
    p("moq-jwp:claim-link", SubjectType::Receipt, false, true, false),
    p("wai:energy-binding-id", SubjectType::Receipt, false, false, false),
    p("wai:video-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:video-manifest-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:video-ssai-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:video-keys-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:video-package-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:video-channel-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:world-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:film-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:haptic-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:score-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:splat4d-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:audio-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:ternary-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:asset-receipt-id", SubjectType::Receipt, true, true, false),
    p("wai:quantum-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:quantum-calibration-id", SubjectType::Receipt, false, true, false),
    p("wai:quantum-job-id", SubjectType::Receipt, false, true, false),
    p("wai:quantum-mitigate-id", SubjectType::Receipt, false, true, false),
    p("wai:quantum-decode-id", SubjectType::Receipt, false, true, false),
    p("wai:quantum-compile-id", SubjectType::Receipt, false, true, false),
    p("wai:quantum-rearrange-id", SubjectType::Receipt, false, true, false),
    p("wai:quantum-fabric-id", SubjectType::Receipt, false, true, false),
    p("wai:phasor-receipt-id", SubjectType::Receipt, false, true, false),
    p("wai:quantum-qbom-id", SubjectType::Receipt, true, true, false),
];

/// The §6.1 table. A function rather than a public constant, so the C header does
/// not gain it.
pub fn profiles() -> &'static [Profile] {
    PROFILES
}

/// The profile named `name`, if §6.1 has one.
pub fn profile(name: &str) -> Option<&'static Profile> {
    PROFILES.iter().find(|p| p.name == name)
}

/// The profile name of an identity domain: the domain without its final version byte.
pub fn profile_name_of_domain(domain: &[u8]) -> Option<&'static str> {
    let (_, name) = domain.split_last()?;
    let name = std::str::from_utf8(name).ok()?;
    profile(name).map(|p| p.name)
}

/// One figure a receipt carries.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Figure {
    /// Its index, in a profile that carries several; `None` in one that carries one.
    pub index: Option<u64>,
    pub joules_micro: u64,
    /// Its class; `None` for an unlabelled or unmetered figure.
    pub class: Option<EnergyProvenance>,
}

/// What a report about a figure is checked against (jwp-receipts §2.4.8, step 6): a
/// receipt that has verified, by profile and identity.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FigureView {
    pub profile: String,
    pub identity: [u8; 32],
    /// For `moq-jwp:object`, the link hash of the group the object was accepted under.
    pub group: Option<[u8; 32]>,
    pub figures: Vec<Figure>,
    /// The Ed25519 key that signed the figures, where one does.
    pub signer: Option<[u8; 32]>,
}

impl FigureView {
    fn single(profile: &str, identity: [u8; 32], joules_micro: u64, class: Option<EnergyProvenance>, signer: [u8; 32]) -> FigureView {
        FigureView {
            profile: profile.into(),
            identity,
            group: None,
            figures: vec![Figure { index: None, joules_micro, class }],
            signer: Some(signer),
        }
    }

    /// The figure at `index`.
    pub fn figure(&self, index: Option<u64>) -> Option<&Figure> {
        self.figures.iter().find(|f| f.index == index)
    }
}

/// A receipt whose figures a report can name.
pub trait EnergyFigures {
    /// The receipt's figures under its profile, or `None` when the receipt does not
    /// verify.
    fn figure_view(&self) -> Option<FigureView>;
}

/// An `energy` claim (`moq-jwp:claim-link`): its figure and class, identified by its
/// link hash; and a `reconstruct-session` claim whose `energy` is not `null`: the
/// session's decode figure and its class (jwp-receipts §2.4.12). A claim of another class,
/// and a session that was not metered, carry no figure.
impl EnergyFigures for ClaimReceipt {
    fn figure_view(&self) -> Option<FigureView> {
        self.verify().ok()?;
        if let ClaimBody::ReconstructSession(s) = &self.body
            && let Some(e) = &s.energy
        {
            let class = e.acquisition_class().ok()?;
            return Some(FigureView::single("moq-jwp:claim-link", self.link_hash(), e.joules_micro, Some(class), self.signer_pubkey));
        }
        let ClaimBody::Energy(e) = &self.body else {
            return Some(FigureView {
                profile: "moq-jwp:claim-link".into(),
                identity: self.link_hash(),
                group: None,
                figures: Vec::new(),
                signer: Some(self.signer_pubkey),
            });
        };
        let class = e.acquisition_class().ok()?;
        Some(FigureView::single("moq-jwp:claim-link", self.link_hash(), e.joules_micro, Some(class), self.signer_pubkey))
    }
}

/// An energy binding's identity: `BLAKE3("wai:energy-binding-id\x01" ‖ JCS(body))`
/// (energy-binding §4). `None` for a body holding a value outside JCS's subset here,
/// such as an integer above 2⁵³ − 1.
pub fn binding_identity(body: &serde_json::Value) -> Option<[u8; 32]> {
    let canonical = jcs(body).ok()?;
    let mut h = blake3::Hasher::new();
    h.update(b"wai:energy-binding-id\x01");
    h.update(canonical.as_bytes());
    Some(*h.finalize().as_bytes())
}

/// An energy binding's body (energy-binding §2), viewed by its identity. No Ed25519
/// key signs it.
pub struct BindingBody<'a>(pub &'a serde_json::Value);

impl EnergyFigures for BindingBody<'_> {
    fn figure_view(&self) -> Option<FigureView> {
        let b = crate::energy_binding::EnergyBinding::from_json(self.0).ok()?;
        Some(FigureView {
            profile: "wai:energy-binding-id".into(),
            identity: binding_identity(self.0)?,
            group: None,
            figures: vec![Figure { index: None, joules_micro: b.microjoules, class: Some(b.acquisition) }],
            signer: None,
        })
    }
}

/// A JWP object (`moq-jwp:object`), accepted under its group with the envelope it was
/// delivered with: its figure at its leaf index, labelled where its leaf is, within
/// the group named by its link hash.
pub struct DeliveredObject<'a> {
    pub object: &'a crate::jwp_delivery::ObjectReceipt,
    pub group: &'a crate::jwp_delivery::GroupReceipt,
    pub envelope: &'a [u8],
}

impl EnergyFigures for DeliveredObject<'_> {
    fn figure_view(&self) -> Option<FigureView> {
        self.object.accept(self.group, self.envelope).ok()?;
        let class = self.object.label.as_ref().and_then(|l| l.class().cloned());
        Some(FigureView {
            profile: "moq-jwp:object".into(),
            identity: self.object.content_hash,
            group: Some(self.group.link_hash()),
            figures: vec![Figure {
                index: Some(self.object.merkle_proof.leaf_index),
                joules_micro: self.object.joules_micro,
                class,
            }],
            signer: Some(self.group.signer_pubkey),
        })
    }
}

/// A JWP object (`moq-jwp:object`) accepted under its group against the content hash the
/// verifier established for it: recomputed over the envelope, or the cid a verified head
/// proof established for an object fetched in part (http-delivery §8.3). Its figure is
/// [`DeliveredObject`]'s.
pub struct HeldObject<'a> {
    pub object: &'a crate::jwp_delivery::ObjectReceipt,
    pub group: &'a crate::jwp_delivery::GroupReceipt,
    pub established: &'a crate::jwp_hash::EstablishedHash,
}

impl EnergyFigures for HeldObject<'_> {
    fn figure_view(&self) -> Option<FigureView> {
        self.object.accept_hash(self.group, self.established).ok()?;
        let class = self.object.label.as_ref().and_then(|l| l.class().cloned());
        Some(FigureView {
            profile: "moq-jwp:object".into(),
            identity: self.object.content_hash,
            group: Some(self.group.link_hash()),
            figures: vec![Figure {
                index: Some(self.object.merkle_proof.leaf_index),
                joules_micro: self.object.joules_micro,
                class,
            }],
            signer: Some(self.group.signer_pubkey),
        })
    }
}

/// The receipt families whose figure sits in `joules_micro` beside an optional or
/// always-present `energy_provenance`, signed by `signer_pubkey`, identified by
/// `receipt_hash()`.
macro_rules! single_figure {
    ($feature:literal, $t:ty, $profile:literal) => {
        #[cfg(feature = $feature)]
        impl EnergyFigures for $t {
            fn figure_view(&self) -> Option<FigureView> {
                if !self.verify() {
                    return None;
                }
                Some(FigureView::single($profile, self.receipt_hash(), self.joules_micro, self.energy_provenance.clone(), self.signer_pubkey))
            }
        }
    };
}

single_figure!("video_receipt", crate::video_receipt::VideoReceipt, "wai:video-receipt-id");
single_figure!("video_manifest", crate::wai_video_manifest::ManifestReceipt, "wai:video-manifest-receipt-id");
single_figure!("video_ssai", crate::wai_video_ssai::SsaiReceipt, "wai:video-ssai-receipt-id");
// The break form (live-ads §8) shares the stitched form's identity domain, so its profile.
single_figure!("video_ssai", crate::wai_video_ssai::SsaiBreakReceipt, "wai:video-ssai-receipt-id");
single_figure!("video_keys", crate::wai_video_keys::KeyReleaseReceipt, "wai:video-keys-receipt-id");
single_figure!("video_package", crate::wai_video_package::PackageReceipt, "wai:video-package-receipt-id");
single_figure!("video_channel", crate::wai_video_channel::ChannelReceipt, "wai:video-channel-receipt-id");
single_figure!("world", crate::world::receipt::SessionReceipt, "wai:world-receipt-id");
single_figure!("film", crate::film::FilmReceipt, "wai:film-receipt-id");
single_figure!("haptic", crate::haptic::HapticReceipt, "wai:haptic-receipt-id");
single_figure!("score", crate::score::ScoreReceipt, "wai:score-receipt-id");
single_figure!("splat4d", crate::splat4d::Splat4dReceipt, "wai:splat4d-receipt-id");
single_figure!("audio_scene", crate::audio_scene::SceneReceipt, "wai:audio-receipt-id");
single_figure!("ternary_receipt", crate::ternary_receipt::TernaryReceipt, "wai:ternary-receipt-id");

/// A version-2 package receipt (packaging §9), viewed with its components' index files
/// in its order: it verifies only against them, so the view holds both. Its profile is
/// version 1's, `wai:video-package-receipt-id`: the two versions share the identity
/// domain.
#[cfg(feature = "video_package")]
impl EnergyFigures for (&crate::package::PackageReceiptV2, &[&[u8]]) {
    fn figure_view(&self) -> Option<FigureView> {
        let (r, indexes) = *self;
        r.verify_detailed(indexes).ok()?;
        Some(FigureView::single("wai:video-package-receipt-id", r.receipt_hash(), r.joules_micro, r.energy_provenance.clone(), r.signer_pubkey))
    }
}

/// A provenance receipt (`wai:asset-receipt-id`): one figure per step, indexed from 0.
impl EnergyFigures for crate::provenance::AssetReceipt {
    fn figure_view(&self) -> Option<FigureView> {
        if !self.verify() {
            return None;
        }
        Some(FigureView {
            profile: "wai:asset-receipt-id".into(),
            identity: self.receipt_hash(),
            group: None,
            figures: self
                .steps
                .iter()
                .enumerate()
                .map(|(i, s)| Figure { index: Some(i as u64), joules_micro: s.joules_micro, class: s.energy_provenance.clone() })
                .collect(),
            signer: Some(self.signer_pubkey),
        })
    }
}

/// A quantum receipt of `wai-quantum`, in either form.
#[cfg(any(feature = "quantum_receipt", feature = "quantum_ops", feature = "quantum_phasor_meter"))]
impl<R: wai_quantum::quantum_energy::Labellable> EnergyFigures for wai_quantum::quantum_energy::MaybeLabelled<R> {
    fn figure_view(&self) -> Option<FigureView> {
        if !self.verify() {
            return None;
        }
        let r = self.receipt();
        let profile = profile_name_of_domain(r.id_domain())?;
        let class = self.energy_class().map(crate::energy_class::from_quantum_class);
        Some(FigureView::single(profile, self.receipt_hash(), r.figure(), class, r.signer_key()))
    }
}

/// A fabric receipt: one figure, labelled, or unmetered at zero.
#[cfg(feature = "quantum_fabric")]
impl EnergyFigures for wai_quantum::quantum_fabric::FabricReceipt {
    fn figure_view(&self) -> Option<FigureView> {
        if !self.verify() {
            return None;
        }
        let class = self.energy_class.as_ref().map(crate::energy_class::from_quantum_class);
        Some(FigureView::single("wai:quantum-fabric-id", self.receipt_hash(), self.joules_micro, class, self.signer_pubkey))
    }
}

/// A quantum bill of materials, unlabelled: one unlabelled figure per entry.
#[cfg(feature = "quantum_qbom")]
impl EnergyFigures for wai_quantum::quantum_qbom::Qbom {
    fn figure_view(&self) -> Option<FigureView> {
        if !self.verify() {
            return None;
        }
        Some(FigureView {
            profile: "wai:quantum-qbom-id".into(),
            identity: self.receipt_hash(),
            group: None,
            figures: self
                .entries
                .iter()
                .enumerate()
                .map(|(i, e)| Figure { index: Some(i as u64), joules_micro: e.joules_micro, class: None })
                .collect(),
            signer: Some(self.signer_pubkey),
        })
    }
}

/// A quantum bill of materials labelled per entry.
#[cfg(feature = "quantum_qbom")]
impl EnergyFigures for wai_quantum::quantum_energy::LabelledQbom {
    fn figure_view(&self) -> Option<FigureView> {
        if !self.verify() {
            return None;
        }
        Some(FigureView {
            profile: "wai:quantum-qbom-id".into(),
            identity: self.receipt_hash(),
            group: None,
            figures: self
                .qbom
                .entries
                .iter()
                .zip(&self.classes)
                .enumerate()
                .map(|(i, (e, c))| Figure {
                    index: Some(i as u64),
                    joules_micro: e.joules_micro,
                    class: c.as_ref().map(crate::energy_class::from_quantum_class),
                })
                .collect(),
            signer: Some(self.qbom.signer_pubkey),
        })
    }
}

/// What a reference meter writes beside the receipt it seals (energy-measurement §6): a
/// `measurement` claim about the receipt's figure, signed with the receipt's key and
/// checked against the receipt before it is written. Kept here, not in the meters, so
/// the whole path is tested in the library.
pub mod meter_report {
    use super::{EnergyFigures, FigureView};
    use crate::claim_receipt::{rfc3339_utc, ClaimBody, ClaimReceipt, FigureHeader, MeasurementClaim};
    use crate::energy_report::{BracketValues, BuildRecord};
    use ed25519_dalek::SigningKey;
    use std::path::PathBuf;

    /// A meter's command line with the report options taken out: `--no-report`, and
    /// `--previous-report <file>`, the meter's previous report, which the new one chains
    /// to (jwp-receipts §3.1 step 5).
    #[derive(Clone, Debug, Default, PartialEq, Eq)]
    pub struct ReportArgs {
        pub positional: Vec<String>,
        pub no_report: bool,
        pub previous_report: Option<String>,
    }

    /// The report options taken out of `args`. `--previous-report` with no file
    /// after it — at the end, or followed by another option — is an error, never a
    /// silent first report.
    pub fn parse_args(args: &[String]) -> Result<ReportArgs, String> {
        let mut out = ReportArgs::default();
        let mut it = args.iter();
        while let Some(a) = it.next() {
            match a.as_str() {
                "--no-report" => out.no_report = true,
                "--previous-report" => match it.next() {
                    Some(p) if !p.starts_with("--") => out.previous_report = Some(p.clone()),
                    _ => return Err("--previous-report needs the previous report's file".into()),
                },
                _ => out.positional.push(a.clone()),
            }
        }
        Ok(out)
    }

    /// [`parse_args`] for a meter's `main`: a usage error exits with status 2.
    pub fn parse_args_or_exit(args: &[String]) -> ReportArgs {
        parse_args(args).unwrap_or_else(|e| {
            eprintln!("usage error: {e}");
            std::process::exit(2)
        })
    }

    /// Where the report goes by default: beside the receipt, its `.json` extension
    /// replaced by `.report.json` (or `.report.json` appended), and to standard output
    /// (`None`) when the receipt is not written to a file.
    pub fn default_path(receipt_out: Option<&str>) -> Option<PathBuf> {
        let r = receipt_out?;
        Some(PathBuf::from(match r.strip_suffix(".json") {
            Some(stem) => format!("{stem}.report.json"),
            None => format!("{r}.report.json"),
        }))
    }

    /// The report on the figure `view` holds at `index`: the figure restated with its
    /// class, the bracket's values, the build and the reader, signed by `signer` — the
    /// key that signed the figure — and checked against `view` (jwp-receipts §3.1 steps
    /// 1–4 and 6). A figure with no class gets no report: `Err` says why.
    #[allow(clippy::too_many_arguments)]
    pub fn emit(
        view: &FigureView,
        index: Option<u64>,
        bracket: Option<BracketValues>,
        build: BuildRecord,
        power_reader: &str,
        signer: &SigningKey,
        signer_id: &str,
        previous: Option<&ClaimReceipt>,
        issued_at: &str,
    ) -> Result<ClaimReceipt, String> {
        emit_with_fabrics(view, index, bracket, build, power_reader, None, signer, signer_id, previous, issued_at)
    }

    /// [`emit`], the report carrying `fabrics` (energy-measurement §6.3): where the work
    /// the figure covers ran, and the figure divided by fabric. `None` is [`emit`]'s
    /// report, which has no `fabrics` member.
    #[allow(clippy::too_many_arguments)]
    pub fn emit_with_fabrics(
        view: &FigureView,
        index: Option<u64>,
        bracket: Option<BracketValues>,
        build: BuildRecord,
        power_reader: &str,
        fabrics: Option<crate::claim_receipt::FabricRecord>,
        signer: &SigningKey,
        signer_id: &str,
        previous: Option<&ClaimReceipt>,
        issued_at: &str,
    ) -> Result<ClaimReceipt, String> {
        let profile = super::profile(&view.profile).ok_or("the receipt's profile is not in energy-measurement section 6.1")?;
        let f = view.figure(index).ok_or("the receipt carries no figure at that index")?;
        let class = f.class.as_ref().ok_or("the figure carries no class (unmetered or unlabelled): it gets no report")?;
        let body = ClaimBody::Measurement(MeasurementClaim {
            figure: FigureHeader::new(profile.name, index, view.group, f.joules_micro, class),
            bracket,
            build,
            power_reader: Some(power_reader.to_owned()),
            fabrics,
        });
        let (seq, parent) = match previous {
            Some(p) => (p.seq + 1, Some(p.link_hash())),
            None => (0, None),
        };
        let r = ClaimReceipt::seal(signer, signer_id, profile.subject_type, view.identity, seq, issued_at, body, parent)
            .map_err(|e| format!("the report is refused ({e})"))?;
        r.check_figure_against(Some(view)).map_err(|e| format!("the report does not check against the receipt ({e})"))?;
        r.check_parent(previous).map_err(|e| format!("the report does not chain to the previous one ({e})"))?;
        Ok(r)
    }

    /// Everything a meter does with its report, after sealing `receipt`: unless
    /// `--no-report` was given, build the report on the figure at `index` and write it to
    /// `explicit` or the default path beside `receipt_out`, or print it; say where it
    /// went, or why there is none. Returns the report written.
    #[allow(clippy::too_many_arguments)]
    pub fn write<R: EnergyFigures>(
        args: &ReportArgs,
        receipt: &R,
        index: Option<u64>,
        bracket: Option<BracketValues>,
        power_reader: &str,
        signer: &SigningKey,
        signer_id: &str,
        receipt_out: Option<&str>,
        explicit: Option<&str>,
    ) -> Option<ClaimReceipt> {
        if args.no_report {
            println!("report            : not written (--no-report); energy-measurement section 6 requires one beside a published figure");
            return None;
        }
        let Some(view) = receipt.figure_view() else {
            eprintln!("report            : none — the receipt does not verify");
            return None;
        };
        let previous = args.previous_report.as_deref().map(|p| {
            let wire = std::fs::read_to_string(p).unwrap_or_else(|e| panic!("read {p}: {e}"));
            crate::claim_receipt::verify_claim_json(&wire).unwrap_or_else(|e| panic!("{p}: {e}"))
        });
        let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
        match emit(&view, index, bracket, BuildRecord::of_this_build(), power_reader, signer, signer_id, previous.as_ref(), &rfc3339_utc(now)) {
            Err(why) => {
                println!("report            : none — {why}");
                None
            }
            Ok(r) => {
                match explicit.map(PathBuf::from).or_else(|| default_path(receipt_out)) {
                    Some(path) => {
                        std::fs::write(&path, r.to_json()).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
                        println!("report            : {}  (measurement claim, checked against the receipt)", path.display());
                    }
                    None => println!("report            : {}", r.to_json()),
                }
                Some(r)
            }
        }
    }
}

#[cfg(test)]
mod tests;
