//! v1 → v2 migration. Idempotent. Never deletes v1 records — links them
//! via `superseded_by` so audit trail is preserved.

use std::process;

use serde::{Deserialize, Serialize};

use super::{load_canaries, mint_v2, save_canaries, CanarySecret, OpError};

/// Per-record migration plan.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationStep {
    pub original_key: String,
    pub action: MigrationAction,
    pub new_key: Option<String>,
    pub reason: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum MigrationAction {
    ReplaceInPlace,
    SkipAlreadyV2,
    SkipAlreadySuperseded,
}

/// Plan for one or more migrations. Generated by [`MigrationService::plan`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationPlan {
    pub steps: Vec<MigrationStep>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationReport {
    pub planned: usize,
    pub executed: usize,
    pub skipped: usize,
    pub failures: Vec<(String, String)>,
}

/// Service orchestrating migration. Stateless — wraps the existing canary store.
pub struct MigrationService;

impl MigrationService {
    /// Build a migration plan. If `only` is `Some`, restrict to that single key.
    /// Otherwise plan every v1 record not yet superseded.
    pub fn plan(only: Option<&str>) -> Result<MigrationPlan, OpError> {
        let store = load_canaries()?;
        let mut steps = Vec::new();
        for (key, c) in &store.canaries {
            if let Some(filter) = only {
                if key != filter {
                    continue;
                }
            }
            steps.push(classify(c, key));
        }
        Ok(MigrationPlan { steps })
    }

    /// Execute the plan. `dry_run = true` prints the plan without modifying state.
    /// Returns a report. Idempotent: re-running on already-migrated record is no-op.
    pub fn execute(
        plan: &MigrationPlan,
        dry_run: bool,
        tool_name: &str,
    ) -> Result<MigrationReport, OpError> {
        let mut report = MigrationReport {
            planned: plan.steps.len(),
            executed: 0,
            skipped: 0,
            failures: Vec::new(),
        };
        for step in &plan.steps {
            match step.action {
                MigrationAction::SkipAlreadyV2 | MigrationAction::SkipAlreadySuperseded => {
                    report.skipped += 1;
                }
                MigrationAction::ReplaceInPlace => {
                    if dry_run {
                        report.skipped += 1;
                        continue;
                    }
                    match supersede_with_v2(&step.original_key, tool_name) {
                        Ok(()) => report.executed += 1,
                        Err(e) => report
                            .failures
                            .push((step.original_key.clone(), e.to_string())),
                    }
                }
            }
        }
        Ok(report)
    }
}

fn classify(c: &CanarySecret, key: &str) -> MigrationStep {
    if c.version == 2 {
        return MigrationStep {
            original_key: key.to_string(),
            action: MigrationAction::SkipAlreadyV2,
            new_key: None,
            reason: "already a v2 record".into(),
        };
    }
    if c.superseded_by.is_some() {
        return MigrationStep {
            original_key: key.to_string(),
            action: MigrationAction::SkipAlreadySuperseded,
            new_key: c.superseded_by.clone(),
            reason: "v1 already superseded by an existing v2 record".into(),
        };
    }
    MigrationStep {
        original_key: key.to_string(),
        action: MigrationAction::ReplaceInPlace,
        new_key: Some(format!("{key}.v2")),
        reason: "v1 record without supersedure link".into(),
    }
}

/// Replace one v1 record with a v2 record using a derived `.v2` key, and link
/// the original via `superseded_by`. The v1 record stays in place for audit.
fn supersede_with_v2(original_key: &str, tool_name: &str) -> Result<(), OpError> {
    let new_key = format!("{original_key}.v2");
    let pid = process::id();
    let (_new_record, _token) = mint_v2(&new_key, tool_name, pid)?;
    // Re-load to also stamp superseded_by on the v1 entry.
    let mut store = load_canaries()?;
    if let Some(orig) = store.canaries.get_mut(original_key) {
        orig.superseded_by = Some(new_key);
        save_canaries(&store)?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ops::canary::{CanarySecret, CanaryStore, PayloadSummary};
    use std::collections::BTreeMap;

    fn v1_record(key: &str) -> CanarySecret {
        CanarySecret {
            key: key.to_string(),
            fake_value: "AKIAIOSFODNN7EXAMPLE".into(),
            created_at: "2026-01-01T00:00:00Z".into(),
            pattern: "aws_key".into(),
            triggered: false,
            trigger_count: 0,
            rotate_after_days: 14,
            version: 1,
            forensic: false,
            superseded_by: None,
            payload_summary: None,
        }
    }

    fn v2_record(key: &str) -> CanarySecret {
        let mut c = v1_record(key);
        c.version = 2;
        c.forensic = true;
        c.pattern = "v2_forensic".into();
        c.payload_summary = Some(PayloadSummary {
            tool_name: "test".into(),
            minted_at: "2026-05-08T00:00:00Z".into(),
            key_version: 1,
            token_prefix: "cnry_AAAA".into(),
        });
        c
    }

    #[test]
    fn classify_skips_v2_record() {
        let r = v2_record("API_KEY");
        let step = classify(&r, "API_KEY");
        assert_eq!(step.action, MigrationAction::SkipAlreadyV2);
    }

    #[test]
    fn classify_skips_already_superseded() {
        let mut r = v1_record("API_KEY");
        r.superseded_by = Some("API_KEY.v2".into());
        let step = classify(&r, "API_KEY");
        assert_eq!(step.action, MigrationAction::SkipAlreadySuperseded);
    }

    #[test]
    fn classify_replaces_v1_without_supersedure() {
        let r = v1_record("API_KEY");
        let step = classify(&r, "API_KEY");
        assert_eq!(step.action, MigrationAction::ReplaceInPlace);
    }

    #[test]
    fn dry_run_executes_no_changes() {
        let plan = MigrationPlan {
            steps: vec![MigrationStep {
                original_key: "X".into(),
                action: MigrationAction::ReplaceInPlace,
                new_key: Some("X.v2".into()),
                reason: "test".into(),
            }],
        };
        let r = MigrationService::execute(&plan, true, "test-tool").unwrap();
        assert_eq!(r.executed, 0);
        assert_eq!(r.skipped, 1);
    }

    #[test]
    fn report_counts_skips() {
        let plan = MigrationPlan {
            steps: vec![
                MigrationStep {
                    original_key: "A".into(),
                    action: MigrationAction::SkipAlreadyV2,
                    new_key: None,
                    reason: "".into(),
                },
                MigrationStep {
                    original_key: "B".into(),
                    action: MigrationAction::SkipAlreadySuperseded,
                    new_key: None,
                    reason: "".into(),
                },
            ],
        };
        let r = MigrationService::execute(&plan, false, "tool").unwrap();
        assert_eq!(r.planned, 2);
        assert_eq!(r.skipped, 2);
        assert_eq!(r.executed, 0);
    }

    #[test]
    fn unused_canarystore_compile_check() {
        // Make sure module re-exports compile against existing types.
        let _ = CanaryStore {
            canaries: BTreeMap::new(),
        };
    }
}
