//! [`FileClaimStore`] — the Phase-0, file-backed [`ClaimStore`]. Whole-ledger
//! fold-on-read, atomic (temp→fsync→rename) cache writes, O_APPEND+fsync ledger.
//! This is the H0a emergency patch for F2 (the write race) behind the seam.

use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};

use crate::store::{ClaimFilter, ClaimStore, EventKind, LedgerEvent, StoreError};
use crate::{Claim, ClaimStatus, aether_dir, claims_dir, ledger_path, load_all};

/// The deterministic, **ledger-only** fold (no file seed) — this is the cross-language
/// conformance contract pinned by `conformance/ledger-fold/`. Per `claim_id`: the latest
/// payload event (the one carrying a `claim`) is the base content; later events apply
/// their status transition. Identical in Rust and Python.
pub fn fold(events: &[LedgerEvent]) -> BTreeMap<String, Claim> {
    fold_into(BTreeMap::new(), events)
}

/// Fold `events` (append order) over a seed map. A payload event resets the base
/// content for its `claim_id`; a status-only event applies on top of whatever base
/// exists (seed file or earlier payload). Status-only events for an unknown claim
/// are ignored (no base to transition).
fn fold_into(
    mut claims: BTreeMap<String, Claim>,
    events: &[LedgerEvent],
) -> BTreeMap<String, Claim> {
    for ev in events {
        if let Some(payload) = &ev.claim {
            let mut c = payload.clone();
            apply_event(&mut c, ev);
            claims.insert(ev.claim_id.clone(), c);
        } else if let Some(c) = claims.get_mut(&ev.claim_id) {
            apply_event(c, ev);
        }
    }
    claims
}

/// Apply one event's status semantics to a claim: `status = event.to`,
/// `updated_at = event.ts`; `claim_proven` also sets `last_verified_at`;
/// `claim_invalidated` (and the rest) **preserve** it. `reverify_queued` is a
/// queue marker, not a content/status change — it is a no-op here.
fn apply_event(c: &mut Claim, ev: &LedgerEvent) {
    if matches!(ev.event, EventKind::ReverifyQueued) {
        return;
    }
    if let Ok(to) = ev.to.parse::<ClaimStatus>() {
        c.status = to;
    }
    c.updated_at = ev.ts.clone();
    if matches!(ev.event, EventKind::ClaimProven) {
        c.last_verified_at = Some(ev.ts.clone());
    }
}

/// Crash-safe write: `<name>.tmp.<pid>` in the same dir → write → `fsync` →
/// `rename` over the target. Never truncates the target in place (the F2 bug).
pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
    let dir = path.parent().unwrap_or_else(|| Path::new("."));
    let name = path
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("claim.json");
    let tmp = dir.join(format!(".{name}.tmp.{}", std::process::id()));
    {
        let mut f = std::fs::File::create(&tmp)?;
        f.write_all(bytes)?;
        f.sync_all()?;
    }
    std::fs::rename(&tmp, path)?;
    Ok(())
}

/// A file-backed claim store rooted at a project (`<root>/.aether/…`).
pub struct FileClaimStore {
    project_root: PathBuf,
    /// RFC3339 timestamp source for emitted events. The crate stays time-free:
    /// callers (the CLI, `invalidate`) inject `now`; the default reads the clock.
    clock: Box<dyn Fn() -> String + Send + Sync>,
}

impl FileClaimStore {
    /// A store whose events are stamped with the system clock (RFC3339 UTC).
    pub fn new(project_root: impl Into<PathBuf>) -> Self {
        Self::with_clock(project_root, system_now_rfc3339)
    }

    /// A store whose events are stamped by `clock` — used to inject a fixed `now`.
    pub fn with_clock(
        project_root: impl Into<PathBuf>,
        clock: impl Fn() -> String + Send + Sync + 'static,
    ) -> Self {
        Self {
            project_root: project_root.into(),
            clock: Box::new(clock),
        }
    }

    /// All ledger events in append (line) order. Empty if there is no ledger yet.
    pub fn read_events(&self) -> Result<Vec<LedgerEvent>, StoreError> {
        let path = ledger_path(&self.project_root);
        if !path.exists() {
            return Ok(Vec::new());
        }
        let txt = std::fs::read_to_string(&path)?;
        let mut out = Vec::new();
        for line in txt.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            let ev: LedgerEvent = serde_json::from_str(line)
                .map_err(|e| StoreError::SchemaViolation(format!("bad ledger line: {e}")))?;
            out.push(ev);
        }
        Ok(out)
    }

    /// The current claims: existing `claims/*.json` files as the seed (so claims that
    /// predate the ledger still appear), then the WHOLE ledger folded on top
    /// (ledger wins). The conformance `fold()` is the ledger-only special case.
    fn fold_state(&self) -> Result<BTreeMap<String, Claim>, StoreError> {
        let mut seed = BTreeMap::new();
        for c in load_all(&self.project_root)? {
            seed.insert(c.id.clone(), c);
        }
        Ok(fold_into(seed, &self.read_events()?))
    }

    /// Fold the WHOLE ledger and write every `claims/<id>.json` atomically.
    /// `claims/` is a pure derived cache, rebuildable from the ledger alone.
    pub fn materialize(&self) -> Result<BTreeMap<String, Claim>, StoreError> {
        let claims = self.fold_state()?;
        let dir = claims_dir(&self.project_root);
        std::fs::create_dir_all(&dir)?;
        for (id, claim) in &claims {
            let path = dir.join(format!("{id}.json"));
            let txt = serde_json::to_string_pretty(claim)
                .map_err(|e| StoreError::SchemaViolation(e.to_string()))?;
            atomic_write(&path, format!("{txt}\n").as_bytes())?;
        }
        Ok(claims)
    }

    /// Append one event to `ledger.jsonl` (O_APPEND, atomic per line) and fsync.
    fn append_line(&self, event: &LedgerEvent) -> Result<(), StoreError> {
        std::fs::create_dir_all(aether_dir(&self.project_root))?;
        let line =
            serde_json::to_string(event).map_err(|e| StoreError::SchemaViolation(e.to_string()))?;
        let mut f = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(ledger_path(&self.project_root))?;
        writeln!(f, "{line}")?;
        f.sync_all()?;
        Ok(())
    }
}

/// The ledger event kind that records a transition INTO `to`.
fn event_kind_for(to: ClaimStatus) -> EventKind {
    match to {
        ClaimStatus::Proposed => EventKind::ClaimProposed,
        ClaimStatus::Proven => EventKind::ClaimProven,
        ClaimStatus::Stale => EventKind::ClaimInvalidated,
        ClaimStatus::Contradicted => EventKind::ClaimContradicted,
        ClaimStatus::Revoked => EventKind::ClaimRevoked,
    }
}

impl ClaimStore for FileClaimStore {
    fn read(&self, id: &str) -> Result<Claim, StoreError> {
        self.fold_state()?
            .remove(id)
            .ok_or_else(|| StoreError::NotFound(id.to_string()))
    }

    fn list(&self, filter: Option<ClaimFilter>) -> Result<Vec<Claim>, StoreError> {
        let mut out: Vec<Claim> = self.fold_state()?.into_values().collect();
        if let Some(f) = filter {
            out.retain(|c| {
                f.status.is_none_or(|s| c.status == s)
                    && f.kind.is_none_or(|k| c.kind == k)
                    && f.subject_prefix
                        .as_ref()
                        .is_none_or(|p| c.subject.starts_with(p))
            });
        }
        Ok(out)
    }

    fn history(&self, id: &str) -> Result<Vec<LedgerEvent>, StoreError> {
        Ok(self
            .read_events()?
            .into_iter()
            .filter(|e| e.claim_id == id)
            .collect())
    }

    fn transition(
        &self,
        id: &str,
        to: ClaimStatus,
        actor: &str,
        evidence: Option<&str>,
    ) -> Result<Claim, StoreError> {
        let current = self.read(id)?;
        let from = current.status;
        if !from.can_transition_to(to) {
            return Err(StoreError::InvalidTransition {
                from: from.as_str().to_string(),
                to: to.as_str().to_string(),
            });
        }
        let event = LedgerEvent {
            ts: (self.clock)(),
            event: event_kind_for(to),
            claim_id: id.to_string(),
            from: Some(from.as_str().to_string()),
            to: to.as_str().to_string(),
            actor: actor.to_string(),
            evidence: evidence.map(str::to_string),
            // Eidos emits status-only events; payload events are Veris's job.
            claim: None,
        };
        self.append_line(&event)?;
        // Append-only: the claim file is NOT written here (readers fold it). Return
        // the folded result in-memory so callers see the new state immediately.
        let mut updated = current;
        apply_event(&mut updated, &event);
        Ok(updated)
    }

    fn append_event(&self, event: LedgerEvent) -> Result<(), StoreError> {
        self.append_line(&event)
    }
}

/// System clock → RFC3339 UTC (`YYYY-MM-DDTHH:MM:SSZ`), dependency-free. Only the
/// default-clock path uses this; `invalidate`/tests inject `now` via `with_clock`.
fn system_now_rfc3339() -> String {
    let secs = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs() as i64)
        .unwrap_or(0);
    rfc3339_from_unix(secs)
}

/// Convert Unix seconds → RFC3339 UTC using Howard Hinnant's civil-from-days
/// algorithm (correct for the full proleptic Gregorian range; no time crate).
fn rfc3339_from_unix(secs: i64) -> String {
    let days = secs.div_euclid(86_400);
    let rem = secs.rem_euclid(86_400);
    let (hh, mm, ss) = (rem / 3600, (rem % 3600) / 60, rem % 60);

    let z = days + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let doe = z - era * 146_097; // [0, 146096]
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
    let year = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
    let mp = (5 * doy + 2) / 153; // [0, 11]
    let day = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
    let month = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
    let year = if month <= 2 { year + 1 } else { year };

    format!("{year:04}-{month:02}-{day:02}T{hh:02}:{mm:02}:{ss:02}Z")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rfc3339_conversion_matches_known_epochs() {
        assert_eq!(rfc3339_from_unix(0), "1970-01-01T00:00:00Z");
        // 2026-06-15T12:00:00Z
        assert_eq!(rfc3339_from_unix(1_781_524_800), "2026-06-15T12:00:00Z");
    }
}
