use std::env;
use std::fs;
use std::fs::OpenOptions;
use std::io;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use chrono::Utc;
use uuid::Uuid;

use crate::model::{
    MentatConfig, SessionAgentRole, SessionFile, SessionImprovementRecord, SessionReflectionRecord,
    SessionTranscriptEntry, SessionTranscriptEntryKind,
};

pub type AppResult<T> = Result<T, Box<dyn std::error::Error>>;

#[derive(Debug, Clone)]
pub struct Storage {
    root: PathBuf,
    transcript_append_lock: Arc<Mutex<()>>,
}

impl Storage {
    pub fn from_env() -> AppResult<Self> {
        Ok(Self {
            root: mentat_root()?,
            transcript_append_lock: Arc::new(Mutex::new(())),
        })
    }

    pub fn from_root(root: PathBuf) -> Self {
        Self {
            root,
            transcript_append_lock: Arc::new(Mutex::new(())),
        }
    }

    pub fn config_path(&self) -> PathBuf {
        self.root.join("config.toml")
    }

    pub fn sessions_dir(&self) -> PathBuf {
        self.root.join("sessions")
    }

    pub fn session_dir(&self, session_id: &str) -> PathBuf {
        self.sessions_dir().join(session_id)
    }

    pub fn session_reflections_dir(&self, session_id: &str) -> PathBuf {
        self.session_dir(session_id).join("reflections")
    }

    pub fn session_improvements_dir(&self, session_id: &str) -> PathBuf {
        self.session_dir(session_id).join("improvements")
    }

    pub fn session_improvement_runtime_dir(
        &self,
        session_id: &str,
        improvement_id: &str,
    ) -> PathBuf {
        self.session_dir(session_id)
            .join("improvement-runtimes")
            .join(improvement_id)
    }

    pub fn session_improvement_events_path(
        &self,
        session_id: &str,
        improvement_id: &str,
    ) -> PathBuf {
        self.session_improvement_runtime_dir(session_id, improvement_id)
            .join("codex-events.ndjson")
    }

    pub fn create_session(&self, cwd: PathBuf) -> AppResult<SessionFile> {
        let session_id = Uuid::new_v4().to_string();
        let now = timestamp();
        let session = SessionFile {
            session_id,
            cwd,
            created_at: now.clone(),
            updated_at: now,
            foreground_turn_id: None,
            mentat: None,
        };
        self.save_session(&session)?;
        fs::write(
            self.session_dir(&session.session_id)
                .join("transcript.ndjson"),
            "",
        )?;
        Ok(session)
    }

    pub fn load_session(&self, session_id: &str) -> AppResult<SessionFile> {
        read_json(self.session_dir(session_id).join("session.json"))
    }

    pub fn save_session(&self, session: &SessionFile) -> AppResult<()> {
        write_json(
            self.session_dir(&session.session_id).join("session.json"),
            session,
        )
    }

    pub fn append_session_transcript_entry(
        &self,
        session_id: &str,
        kind: SessionTranscriptEntryKind,
        agent_role: Option<SessionAgentRole>,
        content: &str,
    ) -> AppResult<()> {
        let _guard = self
            .transcript_append_lock
            .lock()
            .map_err(|_| io::Error::other("transcript append lock poisoned"))?;
        let path = self.session_dir(session_id).join("transcript.ndjson");
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let mut file = OpenOptions::new().create(true).append(true).open(path)?;
        let line = serde_json::to_string(&SessionTranscriptEntry {
            timestamp: timestamp(),
            kind,
            agent_role,
            content: content.to_string(),
        })?;
        writeln!(file, "{line}")?;
        Ok(())
    }

    pub fn append_user_message(&self, session_id: &str, content: &str) -> AppResult<()> {
        self.append_session_transcript_entry(
            session_id,
            SessionTranscriptEntryKind::UserMessage,
            None,
            content,
        )
    }

    pub fn append_agent_message(
        &self,
        session_id: &str,
        agent_role: SessionAgentRole,
        content: &str,
    ) -> AppResult<()> {
        self.append_session_transcript_entry(
            session_id,
            SessionTranscriptEntryKind::AgentMessage,
            Some(agent_role),
            content,
        )
    }

    pub fn append_tool_event(
        &self,
        session_id: &str,
        agent_role: SessionAgentRole,
        content: &str,
    ) -> AppResult<()> {
        self.append_session_transcript_entry(
            session_id,
            SessionTranscriptEntryKind::ToolEvent,
            Some(agent_role),
            content,
        )
    }

    pub fn append_divider(&self, session_id: &str, content: &str) -> AppResult<()> {
        self.append_session_transcript_entry(
            session_id,
            SessionTranscriptEntryKind::Divider,
            None,
            content,
        )
    }

    pub fn append_system_message(&self, session_id: &str, content: &str) -> AppResult<()> {
        self.append_session_transcript_entry(
            session_id,
            SessionTranscriptEntryKind::SystemMessage,
            None,
            content,
        )
    }

    pub fn load_session_transcript_entries(
        &self,
        session_id: &str,
    ) -> AppResult<Vec<SessionTranscriptEntry>> {
        let path = self.session_dir(session_id).join("transcript.ndjson");
        if !path.exists() {
            return Ok(Vec::new());
        }

        let content = fs::read_to_string(path)?;
        let mut entries = Vec::new();
        for line in content.lines() {
            if line.trim().is_empty() {
                continue;
            }
            entries.push(serde_json::from_str::<SessionTranscriptEntry>(line)?);
        }
        Ok(entries)
    }

    pub fn load_config(&self) -> AppResult<MentatConfig> {
        let config_path = self.config_path();
        if !config_path.exists() {
            return Ok(MentatConfig::default());
        }

        let config_text = fs::read_to_string(&config_path).map_err(|error| {
            io::Error::new(
                error.kind(),
                format!("failed to read {}: {error}", config_path.display()),
            )
        })?;
        let config: MentatConfig = toml::from_str(&config_text)?;
        Ok(config)
    }

    pub fn save_session_reflection(
        &self,
        session_id: &str,
        reflection: &SessionReflectionRecord,
    ) -> AppResult<()> {
        fs::create_dir_all(self.session_reflections_dir(session_id))?;
        write_json(
            self.session_reflections_dir(session_id)
                .join(format!("{}.json", reflection.reflection_id)),
            reflection,
        )
    }

    pub fn load_session_reflection(
        &self,
        session_id: &str,
        reflection_id: &str,
    ) -> AppResult<SessionReflectionRecord> {
        read_json(
            self.session_reflections_dir(session_id)
                .join(format!("{reflection_id}.json")),
        )
    }

    pub fn save_session_improvement(
        &self,
        session_id: &str,
        improvement: &SessionImprovementRecord,
    ) -> AppResult<()> {
        fs::create_dir_all(self.session_improvements_dir(session_id))?;
        write_json(
            self.session_improvements_dir(session_id)
                .join(format!("{}.json", improvement.improvement_id)),
            improvement,
        )
    }

    pub fn load_session_improvement(
        &self,
        session_id: &str,
        improvement_id: &str,
    ) -> AppResult<SessionImprovementRecord> {
        read_json(
            self.session_improvements_dir(session_id)
                .join(format!("{improvement_id}.json")),
        )
    }

    pub fn update_session_improvement(
        &self,
        session_id: &str,
        improvement: &SessionImprovementRecord,
    ) -> AppResult<()> {
        let path = self
            .session_improvements_dir(session_id)
            .join(format!("{}.json", improvement.improvement_id));
        if !path.exists() {
            return Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!(
                    "session improvement {} does not exist for session {}",
                    improvement.improvement_id, session_id
                ),
            )
            .into());
        }
        write_json(path, improvement)
    }

    pub fn list_session_improvements(
        &self,
        session_id: &str,
    ) -> AppResult<Vec<SessionImprovementRecord>> {
        let dir = self.session_improvements_dir(session_id);
        if !dir.exists() {
            return Ok(Vec::new());
        }

        let mut improvements: Vec<SessionImprovementRecord> = Vec::new();
        for entry in fs::read_dir(dir)? {
            let entry = entry?;
            if entry.path().extension().and_then(|ext| ext.to_str()) != Some("json") {
                continue;
            }
            improvements.push(read_json(entry.path())?);
        }
        improvements.sort_by(|left, right| left.created_at.cmp(&right.created_at));
        Ok(improvements)
    }
}

fn mentat_root() -> AppResult<PathBuf> {
    if let Ok(root) = env::var("MENTAI_ROOT") {
        return Ok(PathBuf::from(root));
    }

    let home = env::var("HOME")
        .map(PathBuf::from)
        .map_err(|_| io::Error::new(io::ErrorKind::NotFound, "HOME is not set"))?;
    Ok(home.join(".mentai"))
}

fn read_json<T>(path: PathBuf) -> AppResult<T>
where
    T: serde::de::DeserializeOwned,
{
    let content = fs::read_to_string(&path).map_err(|error| {
        io::Error::new(
            error.kind(),
            format!("failed to read {}: {error}", path.display()),
        )
    })?;
    Ok(serde_json::from_str(&content)?)
}

fn write_json<T>(path: PathBuf, value: &T) -> AppResult<()>
where
    T: serde::Serialize,
{
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let json = serde_json::to_string_pretty(value)?;
    let temp_path = path.with_extension("tmp");
    fs::write(&temp_path, format!("{json}\n"))?;
    fs::rename(temp_path, path)?;
    Ok(())
}

pub fn timestamp() -> String {
    Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
}

#[cfg(test)]
mod tests {
    use super::Storage;
    use crate::model::SessionAgentRole;
    use std::fs;
    use std::sync::{Arc, Barrier};
    use std::thread;
    use tempfile::TempDir;

    #[test]
    fn load_config_defaults_when_missing() {
        let tempdir = TempDir::new().expect("create tempdir");
        let storage = Storage::from_root(tempdir.path().join("mentat-root"));

        let config = storage.load_config().expect("load default config");
        assert!(config.codex.is_none());
    }

    #[test]
    fn load_config_reads_config_file() {
        let tempdir = TempDir::new().expect("create tempdir");
        let root = tempdir.path().join("mentat-root");
        fs::create_dir_all(&root).expect("create root");
        let storage = Storage::from_root(root.clone());
        fs::write(
            storage.config_path(),
            "[codex]\nmodel = \"gpt-5.4\"\n[codex.permissions]\nnetwork_access = true\n",
        )
        .expect("write config");

        let config = storage.load_config().expect("load config");
        let codex = config.codex.expect("codex config");
        assert_eq!(codex.model.as_deref(), Some("gpt-5.4"));
        assert_eq!(
            codex
                .permissions
                .and_then(|permissions| permissions.network_access),
            Some(true)
        );
    }

    #[test]
    fn concurrent_transcript_appends_do_not_drop_lines() {
        let tempdir = TempDir::new().expect("create tempdir");
        let root = tempdir.path().join("mentat-root");
        let workspace = tempdir.path().join("workspace");
        fs::create_dir_all(&root).expect("create root");
        fs::create_dir_all(&workspace).expect("create workspace");
        let storage = Storage::from_root(root.clone());
        let session = storage.create_session(workspace).expect("create session");

        let rounds = 8usize;
        let writers = 12usize;
        let mut handles = Vec::new();
        let mut barriers = Vec::new();
        for _ in 0..rounds {
            barriers.push(Arc::new(Barrier::new(writers)));
        }

        for writer in 0..writers {
            let storage = storage.clone();
            let session_id = session.session_id.clone();
            let barriers = barriers.clone();
            handles.push(thread::spawn(move || {
                for (round, barrier) in barriers.into_iter().enumerate() {
                    barrier.wait();
                    storage
                        .append_agent_message(
                            &session_id,
                            SessionAgentRole::Worker,
                            &format!("writer-{writer}-round-{round}"),
                        )
                        .expect("append transcript line");
                }
            }));
        }

        for handle in handles {
            handle.join().expect("join writer");
        }

        let transcript = fs::read_to_string(
            root.join("sessions")
                .join(&session.session_id)
                .join("transcript.ndjson"),
        )
        .expect("read transcript");
        let line_count = transcript
            .lines()
            .filter(|line| !line.trim().is_empty())
            .count();

        assert_eq!(
            line_count,
            rounds * writers,
            "concurrent transcript appends lost lines:\n{transcript}"
        );
    }

    #[test]
    fn create_session_uses_uuid_session_id() {
        let tempdir = TempDir::new().expect("create tempdir");
        let root = tempdir.path().join("mentat-root");
        let workspace = tempdir.path().join("workspace");
        fs::create_dir_all(&root).expect("create root");
        fs::create_dir_all(&workspace).expect("create workspace");
        let storage = Storage::from_root(root);

        let session = storage.create_session(workspace).expect("create session");

        assert!(
            looks_like_uuid(&session.session_id),
            "expected UUID session id, got {}",
            session.session_id
        );
    }

    fn looks_like_uuid(value: &str) -> bool {
        let chars = value.chars().collect::<Vec<_>>();
        if chars.len() != 36 {
            return false;
        }

        for &index in &[8usize, 13, 18, 23] {
            if chars[index] != '-' {
                return false;
            }
        }

        chars
            .iter()
            .enumerate()
            .all(|(index, ch)| matches!(index, 8 | 13 | 18 | 23) || ch.is_ascii_hexdigit())
    }
}
