use crate::contradiction::ContradictionHit;
use crate::domain::{MemoryLifecycleState, MemoryRecord};
use crate::lifecycle_store::{
    LedgerEntry, LifecycleStore, ProposeMemoryRequest, RecordMemoryRequest, TransitionMetadata,
    accept_memory_with_metadata, archive_memory_with_metadata, latest_state_entries,
    lifecycle_root_from_config, pending_review_entries, promote_memory_to_canonical_with_metadata,
    propose_ai_memory, read_events_for_record, record_manual_memory, wakeup_ready_entries,
};
use serde::Serialize;
use std::path::Path;
use ts_rs::TS;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
pub enum LifecycleAction {
    Accept,
    PromoteToCanonical,
    Archive,
}

impl LifecycleAction {
    pub fn label(self) -> &'static str {
        match self {
            Self::Accept => "accept",
            Self::PromoteToCanonical => "promote",
            Self::Archive => "archive",
        }
    }
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq, TS)]
#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
pub struct LifecycleWorkbenchSnapshot {
    pub pending_review: Vec<LedgerEntry>,
    pub wakeup_ready: Vec<LedgerEntry>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LifecycleWriteResult {
    pub entry: LedgerEntry,
    pub snapshot: LifecycleWorkbenchSnapshot,
    pub contradictions: Vec<ContradictionHit>,
}

#[derive(Debug, Default, Clone, Copy)]
pub struct LifecycleService;

impl LifecycleService {
    pub fn new() -> Self {
        Self
    }

    pub fn load_workbench(self, config_path: &Path) -> anyhow::Result<LifecycleWorkbenchSnapshot> {
        let store = self.store_for_config(config_path);
        Ok(LifecycleWorkbenchSnapshot {
            pending_review: pending_review_entries(&store)?,
            wakeup_ready: wakeup_ready_entries(&store)?,
        })
    }

    pub fn apply_action(
        self,
        config_path: &Path,
        record_id: &str,
        action: LifecycleAction,
    ) -> anyhow::Result<LifecycleWriteResult> {
        self.apply_action_with_metadata(
            config_path,
            record_id,
            action,
            TransitionMetadata::default(),
        )
    }

    pub fn apply_action_with_metadata(
        self,
        config_path: &Path,
        record_id: &str,
        action: LifecycleAction,
        metadata: TransitionMetadata,
    ) -> anyhow::Result<LifecycleWriteResult> {
        let store = self.store_for_config(config_path);
        let entry = match action {
            LifecycleAction::Accept => accept_memory_with_metadata(&store, record_id, metadata)?,
            LifecycleAction::PromoteToCanonical => {
                promote_memory_to_canonical_with_metadata(&store, record_id, metadata)?
            }
            LifecycleAction::Archive => archive_memory_with_metadata(&store, record_id, metadata)?,
        };
        let snapshot = self.load_workbench(config_path)?;
        Ok(LifecycleWriteResult {
            entry,
            snapshot,
            contradictions: Vec::new(),
        })
    }

    pub fn record_manual(
        self,
        config_path: &Path,
        request: RecordMemoryRequest,
    ) -> anyhow::Result<LifecycleWriteResult> {
        let store = self.store_for_config(config_path);
        let entry = record_manual_memory(&store, request)?;
        let snapshot = self.load_workbench(config_path)?;
        let existing: Vec<(String, MemoryRecord)> = wakeup_ready_entries(&store)
            .unwrap_or_default()
            .into_iter()
            .map(|e| (e.record_id, e.record))
            .collect();
        let contradictions = crate::contradiction::detect(
            &entry.record.summary,
            &entry.record.memory_type,
            &existing,
        );
        Ok(LifecycleWriteResult {
            entry,
            snapshot,
            contradictions,
        })
    }

    pub fn propose_ai(
        self,
        config_path: &Path,
        request: ProposeMemoryRequest,
    ) -> anyhow::Result<LifecycleWriteResult> {
        let store = self.store_for_config(config_path);
        let entry = propose_ai_memory(&store, request)?;
        let snapshot = self.load_workbench(config_path)?;
        let existing: Vec<(String, MemoryRecord)> = wakeup_ready_entries(&store)
            .unwrap_or_default()
            .into_iter()
            .map(|e| (e.record_id, e.record))
            .collect();
        let contradictions = crate::contradiction::detect(
            &entry.record.summary,
            &entry.record.memory_type,
            &existing,
        );
        Ok(LifecycleWriteResult {
            entry,
            snapshot,
            contradictions,
        })
    }

    pub fn get_record(
        self,
        config_path: &Path,
        record_id: &str,
    ) -> anyhow::Result<Option<LedgerEntry>> {
        let store = self.store_for_config(config_path);
        Ok(latest_state_entries(&store)?
            .into_iter()
            .find(|entry| entry.record_id == record_id))
    }

    pub fn get_history(
        self,
        config_path: &Path,
        record_id: &str,
    ) -> anyhow::Result<Vec<LedgerEntry>> {
        let store = self.store_for_config(config_path);
        read_events_for_record(&store, record_id)
    }

    fn store_for_config(self, config_path: &Path) -> LifecycleStore {
        let config_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
        let lifecycle_root = lifecycle_root_from_config(config_dir);
        LifecycleStore::new(lifecycle_root.as_path())
    }
}

pub fn available_actions(record: &MemoryRecord) -> &'static [LifecycleAction] {
    match record.state {
        MemoryLifecycleState::Candidate => &[LifecycleAction::Accept, LifecycleAction::Archive],
        MemoryLifecycleState::Accepted => &[
            LifecycleAction::PromoteToCanonical,
            LifecycleAction::Archive,
        ],
        MemoryLifecycleState::Canonical => &[LifecycleAction::Archive],
        MemoryLifecycleState::Draft | MemoryLifecycleState::Archived => &[],
    }
}

#[cfg(test)]
mod tests {
    use super::{LifecycleAction, LifecycleService, available_actions};
    use crate::domain::{MemoryLifecycleState, MemoryScope};
    use crate::lifecycle_store::{
        LifecycleStore, ProposeMemoryRequest, RecordMemoryRequest, TransitionMetadata,
        lifecycle_root_from_config, propose_ai_memory, read_events_for_record,
        record_manual_memory,
    };
    use std::fs;
    use tempfile::tempdir;

    fn setup_config_path() -> (tempfile::TempDir, std::path::PathBuf, LifecycleStore) {
        let temp = tempdir().unwrap();
        let config_path = temp.path().join("spool.toml");
        fs::write(&config_path, "[vault]\nroot = \"/tmp\"\n").unwrap();
        let store = LifecycleStore::new(lifecycle_root_from_config(temp.path()).as_path());
        (temp, config_path, store)
    }

    #[test]
    fn service_should_load_pending_review_and_wakeup_ready() {
        let (_temp, config_path, store) = setup_config_path();
        let _ = record_manual_memory(
            &store,
            RecordMemoryRequest {
                title: "简洁输出".to_string(),
                summary: "偏好简洁".to_string(),
                memory_type: "preference".to_string(),
                scope: MemoryScope::User,
                source_ref: "manual:gui".to_string(),
                project_id: None,
                user_id: Some("long".to_string()),
                sensitivity: None,
                metadata: TransitionMetadata::default(),
                entities: Vec::new(),
                tags: Vec::new(),
                triggers: Vec::new(),
                related_files: Vec::new(),
                related_records: Vec::new(),
                supersedes: None,
                applies_to: Vec::new(),
                valid_until: None,
            },
        )
        .unwrap();
        let _ = propose_ai_memory(
            &store,
            ProposeMemoryRequest {
                title: "测试偏好".to_string(),
                summary: "先 smoke 再收口".to_string(),
                memory_type: "workflow".to_string(),
                scope: MemoryScope::User,
                source_ref: "session:1".to_string(),
                project_id: None,
                user_id: Some("long".to_string()),
                sensitivity: None,
                metadata: TransitionMetadata::default(),
                entities: Vec::new(),
                tags: Vec::new(),
                triggers: Vec::new(),
                related_files: Vec::new(),
                related_records: Vec::new(),
                supersedes: None,
                applies_to: Vec::new(),
                valid_until: None,
            },
        )
        .unwrap();

        let snapshot = LifecycleService::new()
            .load_workbench(config_path.as_path())
            .unwrap();
        assert_eq!(snapshot.pending_review.len(), 1);
        assert_eq!(snapshot.wakeup_ready.len(), 1);
    }

    #[test]
    fn service_should_apply_action_and_refresh_snapshot() {
        let (_temp, config_path, store) = setup_config_path();
        let proposal = propose_ai_memory(
            &store,
            ProposeMemoryRequest {
                title: "测试偏好".to_string(),
                summary: "先 smoke 再收口".to_string(),
                memory_type: "workflow".to_string(),
                scope: MemoryScope::User,
                source_ref: "session:1".to_string(),
                project_id: None,
                user_id: Some("long".to_string()),
                sensitivity: None,
                metadata: TransitionMetadata::default(),
                entities: Vec::new(),
                tags: Vec::new(),
                triggers: Vec::new(),
                related_files: Vec::new(),
                related_records: Vec::new(),
                supersedes: None,
                applies_to: Vec::new(),
                valid_until: None,
            },
        )
        .unwrap();

        let result = LifecycleService::new()
            .apply_action(
                config_path.as_path(),
                &proposal.record_id,
                LifecycleAction::Accept,
            )
            .unwrap();
        assert_eq!(result.entry.record.state, MemoryLifecycleState::Accepted);
        assert!(result.snapshot.pending_review.is_empty());
        assert_eq!(result.snapshot.wakeup_ready.len(), 1);
    }

    #[test]
    fn service_should_record_manual_memory_and_refresh_snapshot() {
        let (_temp, config_path, _store) = setup_config_path();
        let result = LifecycleService::new()
            .record_manual(
                config_path.as_path(),
                RecordMemoryRequest {
                    title: "简洁输出".to_string(),
                    summary: "偏好简洁".to_string(),
                    memory_type: "preference".to_string(),
                    scope: MemoryScope::User,
                    source_ref: "manual:cli".to_string(),
                    project_id: None,
                    user_id: Some("long".to_string()),
                    sensitivity: Some("internal".to_string()),
                    metadata: TransitionMetadata {
                        actor: Some("codex".to_string()),
                        reason: Some("capture stable preference".to_string()),
                        evidence_refs: vec!["obsidian://note".to_string()],
                    },
                    entities: Vec::new(),
                    tags: Vec::new(),
                    triggers: Vec::new(),
                    related_files: Vec::new(),
                    related_records: Vec::new(),
                    supersedes: None,
                    applies_to: Vec::new(),
                    valid_until: None,
                },
            )
            .unwrap();

        assert_eq!(result.entry.record.state, MemoryLifecycleState::Accepted);
        assert_eq!(result.entry.metadata.actor.as_deref(), Some("codex"));
        assert!(result.snapshot.pending_review.is_empty());
        assert_eq!(result.snapshot.wakeup_ready.len(), 1);
    }

    #[test]
    fn service_should_propose_ai_memory_and_refresh_snapshot() {
        let (_temp, config_path, _store) = setup_config_path();
        let result = LifecycleService::new()
            .propose_ai(
                config_path.as_path(),
                ProposeMemoryRequest {
                    title: "测试偏好".to_string(),
                    summary: "先 smoke 再收口".to_string(),
                    memory_type: "workflow".to_string(),
                    scope: MemoryScope::User,
                    source_ref: "session:1".to_string(),
                    project_id: Some("spool".to_string()),
                    user_id: Some("long".to_string()),
                    sensitivity: None,
                    metadata: TransitionMetadata::default(),
                    entities: Vec::new(),
                    tags: Vec::new(),
                    triggers: Vec::new(),
                    related_files: Vec::new(),
                    related_records: Vec::new(),
                    supersedes: None,
                    applies_to: Vec::new(),
                    valid_until: None,
                },
            )
            .unwrap();

        assert_eq!(result.entry.record.state, MemoryLifecycleState::Candidate);
        assert_eq!(result.snapshot.pending_review.len(), 1);
        assert!(result.snapshot.wakeup_ready.is_empty());
    }

    #[test]
    fn service_should_return_record_history_in_event_order() {
        let (_temp, config_path, _store) = setup_config_path();
        let proposal = LifecycleService::new()
            .propose_ai(
                config_path.as_path(),
                ProposeMemoryRequest {
                    title: "测试偏好".to_string(),
                    summary: "先 smoke 再收口".to_string(),
                    memory_type: "workflow".to_string(),
                    scope: MemoryScope::User,
                    source_ref: "session:1".to_string(),
                    project_id: None,
                    user_id: Some("long".to_string()),
                    sensitivity: None,
                    metadata: TransitionMetadata::default(),
                    entities: Vec::new(),
                    tags: Vec::new(),
                    triggers: Vec::new(),
                    related_files: Vec::new(),
                    related_records: Vec::new(),
                    supersedes: None,
                    applies_to: Vec::new(),
                    valid_until: None,
                },
            )
            .unwrap();
        let _ = LifecycleService::new()
            .apply_action(
                config_path.as_path(),
                &proposal.entry.record_id,
                LifecycleAction::Accept,
            )
            .unwrap();

        let history = LifecycleService::new()
            .get_history(config_path.as_path(), &proposal.entry.record_id)
            .unwrap();
        assert_eq!(history.len(), 2);
        assert_eq!(
            history[0].action,
            crate::domain::MemoryLedgerAction::ProposeAi
        );
        assert_eq!(history[1].action, crate::domain::MemoryLedgerAction::Accept);
    }

    #[test]
    fn service_should_reject_invalid_transition_without_append() {
        let (_temp, config_path, store) = setup_config_path();
        let manual = record_manual_memory(
            &store,
            RecordMemoryRequest {
                title: "简洁输出".to_string(),
                summary: "偏好简洁".to_string(),
                memory_type: "preference".to_string(),
                scope: MemoryScope::User,
                source_ref: "manual:gui".to_string(),
                project_id: None,
                user_id: Some("long".to_string()),
                sensitivity: None,
                metadata: TransitionMetadata::default(),
                entities: Vec::new(),
                tags: Vec::new(),
                triggers: Vec::new(),
                related_files: Vec::new(),
                related_records: Vec::new(),
                supersedes: None,
                applies_to: Vec::new(),
                valid_until: None,
            },
        )
        .unwrap();

        let error = LifecycleService::new()
            .apply_action(
                config_path.as_path(),
                &manual.record_id,
                LifecycleAction::Accept,
            )
            .unwrap_err();
        assert!(error.to_string().contains("invalid lifecycle transition"));
        assert_eq!(
            read_events_for_record(&store, &manual.record_id)
                .unwrap()
                .len(),
            1
        );
    }

    #[test]
    fn available_actions_should_follow_lifecycle_state() {
        let candidate = crate::domain::MemoryRecord::new_ai_proposal(
            "候选",
            "摘要",
            "workflow",
            MemoryScope::User,
            "session:1",
        );
        let accepted = candidate
            .clone()
            .apply(crate::domain::MemoryPromotionAction::Accept);
        let canonical = accepted
            .clone()
            .apply(crate::domain::MemoryPromotionAction::PromoteToCanonical);

        assert_eq!(
            available_actions(&candidate),
            &[LifecycleAction::Accept, LifecycleAction::Archive]
        );
        assert_eq!(
            available_actions(&accepted),
            &[
                LifecycleAction::PromoteToCanonical,
                LifecycleAction::Archive
            ]
        );
        assert_eq!(available_actions(&canonical), &[LifecycleAction::Archive]);
    }

    #[test]
    fn service_should_apply_action_with_metadata() {
        let (_temp, config_path, _store) = setup_config_path();
        let proposal = LifecycleService::new()
            .propose_ai(
                config_path.as_path(),
                ProposeMemoryRequest {
                    title: "测试偏好".to_string(),
                    summary: "先 smoke 再收口".to_string(),
                    memory_type: "workflow".to_string(),
                    scope: MemoryScope::User,
                    source_ref: "session:1".to_string(),
                    project_id: None,
                    user_id: Some("long".to_string()),
                    sensitivity: None,
                    metadata: TransitionMetadata::default(),
                    entities: Vec::new(),
                    tags: Vec::new(),
                    triggers: Vec::new(),
                    related_files: Vec::new(),
                    related_records: Vec::new(),
                    supersedes: None,
                    applies_to: Vec::new(),
                    valid_until: None,
                },
            )
            .unwrap();

        let result = LifecycleService::new()
            .apply_action_with_metadata(
                config_path.as_path(),
                &proposal.entry.record_id,
                LifecycleAction::Accept,
                TransitionMetadata {
                    actor: Some("long".to_string()),
                    reason: Some("confirmed from repeated sessions".to_string()),
                    evidence_refs: vec!["session:1".to_string(), "session:2".to_string()],
                },
            )
            .unwrap();

        assert_eq!(result.entry.metadata.actor.as_deref(), Some("long"));
        assert_eq!(
            result.entry.metadata.reason.as_deref(),
            Some("confirmed from repeated sessions")
        );
        assert_eq!(result.entry.metadata.evidence_refs.len(), 2);
    }
}
