//! Registry lineage for orchestration families (C18/C21): a family — a sweep,
//! Monte-Carlo, or walk-forward run — is a set of run records sharing one
//! parent identity — the `(family, run)` pair, from which the user-facing
//! `family_id` handle (`"{family}-{run}"`) is derived. This module adds the
//! persisted lineage
//! unit ([`FamilyRunRecord`]), the family-store accessors on [`Registry`]
//! ([`Registry::append_family`] / [`Registry::load_family_members`]), and the
//! round-trip that re-derives a [`Family`] from the stored links
//! ([`group_families`]). The family store is a sibling JSONL of the flat runs
//! store (`families.jsonl`), so the flat `runs.jsonl` path and API are untouched.

use std::collections::HashMap;
use std::fs;
use std::io::Write;

use aura_engine::{McFamily, RunReport, SweepFamily, WalkForwardResult};
use serde::{Deserialize, Serialize};

use crate::{Registry, RegistryError};

/// Which C12 orchestration axis produced a family.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum FamilyKind {
    Sweep,
    MonteCarlo,
    WalkForward,
}

/// One persisted family member: a run record stamped with its lineage link.
/// The parent reference is split into its two factors — `family` (the parent
/// name) and `run` (the per-name run index) — from which the user-facing
/// `family_id` handle (`"{family}-{run}"`) is derived rather than stored.
/// `ordinal` is the member's position in the family's canonical order
/// (odometer / seed-input / roll).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FamilyRunRecord {
    pub family: String,
    pub run: usize,
    pub kind: FamilyKind,
    pub ordinal: usize,
    pub report: RunReport,
}

impl FamilyRunRecord {
    /// The user-facing family handle — `"{family}-{run}"`, derived, never stored or parsed.
    pub fn family_id(&self) -> String {
        format!("{}-{}", self.family, self.run)
    }
}

/// A re-derived family: the members sharing one `(family, run)` key (and thus one
/// derived `family_id`), ordinal-sorted — the round-trip result of
/// [`group_families`].
#[derive(Clone, Debug, PartialEq)]
pub struct Family {
    pub id: String,
    pub kind: FamilyKind,
    pub members: Vec<FamilyRunRecord>,
}

impl Registry {
    /// Assign a fresh per-name `run` index (one past the highest `run` already
    /// stored for `name`, or `0` if unseen), stamp `reports` as ordinal-ordered
    /// members carrying `name` + that `run`, and append them as N JSONL lines to
    /// the family store (a sibling of the flat runs store, `families.jsonl`,
    /// leaving the flat path untouched). Returns the derived id `"{name}-{run}"`.
    /// Reads the store once to pick the run index (a read-before-write;
    /// single-process CLI invocations do not race).
    pub fn append_family(
        &self,
        name: &str,
        kind: FamilyKind,
        reports: &[RunReport],
    ) -> Result<String, RegistryError> {
        let run = next_run(name, &self.load_family_members()?);

        let path = self.path.with_file_name("families.jsonl");
        if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
            fs::create_dir_all(parent)?;
        }
        let mut file = fs::OpenOptions::new().create(true).append(true).open(&path)?;
        for (ordinal, report) in reports.iter().enumerate() {
            let record = FamilyRunRecord {
                family: name.to_string(),
                run,
                kind,
                ordinal,
                report: report.clone(),
            };
            let line = serde_json::to_string(&record).expect("a finite FamilyRunRecord serializes");
            writeln!(file, "{line}")?;
        }
        Ok(format!("{name}-{run}"))
    }

    /// Parse every stored family member, in file order. A missing file is an empty
    /// family store (`Ok(vec![])`), exactly as [`Registry::load`] treats a missing
    /// flat store.
    pub fn load_family_members(&self) -> Result<Vec<FamilyRunRecord>, RegistryError> {
        let path = self.path.with_file_name("families.jsonl");
        let text = match fs::read_to_string(&path) {
            Ok(t) => t,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
            Err(e) => return Err(RegistryError::Io(e)),
        };
        let mut records = Vec::new();
        for (i, raw) in text.lines().enumerate() {
            if raw.trim().is_empty() {
                continue;
            }
            let record = serde_json::from_str(raw)
                .map_err(|source| RegistryError::Parse { line: i + 1, source })?;
            records.push(record);
        }
        Ok(records)
    }
}

/// The next per-name run index: one past the highest `run` already stored for
/// `name`, or `0` if the name is unseen. A clean numeric max over the `run`
/// field — no string parsing (the old fused-id form had to generate-and-check to
/// stay robust to `-` inside a name; the split removes that concern).
fn next_run(name: &str, records: &[FamilyRunRecord]) -> usize {
    records
        .iter()
        .filter(|r| r.family == name)
        .map(|r| r.run + 1)
        .max()
        .unwrap_or(0)
}

/// Group member records into families by their `(family, run)` key (first-seen
/// family order; each family's members ordinal-sorted), deriving the family's
/// `id` once at construction. The round-trip: `append_family(...)` then
/// `group_families(load_family_members())` recovers the families.
pub fn group_families(records: Vec<FamilyRunRecord>) -> Vec<Family> {
    let mut order: Vec<(String, usize)> = Vec::new();
    let mut by_key: HashMap<(String, usize), Family> = HashMap::new();
    for rec in records {
        let key = (rec.family.clone(), rec.run);
        let fam = by_key.entry(key.clone()).or_insert_with(|| {
            order.push(key.clone());
            Family { id: rec.family_id(), kind: rec.kind, members: Vec::new() }
        });
        fam.members.push(rec);
    }
    let mut families: Vec<Family> =
        order.into_iter().map(|k| by_key.remove(&k).expect("key was inserted")).collect();
    for fam in &mut families {
        fam.members.sort_by_key(|m| m.ordinal);
    }
    families
}

/// The member reports of a sweep family, in enumeration order — the trivial
/// extractor the CLI hands to [`Registry::append_family`].
pub fn sweep_member_reports(family: &SweepFamily) -> Vec<RunReport> {
    family.points.iter().map(|p| p.report.clone()).collect()
}

/// The member reports of a Monte-Carlo family, in seed-input order.
pub fn mc_member_reports(family: &McFamily) -> Vec<RunReport> {
    family.draws.iter().map(|d| d.report.clone()).collect()
}

/// The per-window OOS reports of a walk-forward result, in roll order.
pub fn walkforward_member_reports(result: &WalkForwardResult) -> Vec<RunReport> {
    result.windows.iter().map(|w| w.run.oos_report.clone()).collect()
}
