use std::fs::{self, OpenOptions};
use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};

use chrono::{SecondsFormat, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{Error, Result};

pub const WORKSPACE_IDENTITY_RELATIVE_PATH: &str = ".yoi/workspace.toml";

/// Stable local Workspace identity persisted as a tracked, safe project record.
///
/// The v0 TOML schema contains identity metadata plus optional tracked project
/// policy tables such as `[ticket]`. Runtime/local-only settings remain rejected
/// here because this loader cannot safely round-trip future local runtime settings
/// without risking accidental path or secret persistence.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceIdentity {
    pub workspace_id: String,
    pub created_at: String,
    pub display_name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct WorkspaceIdentityFile {
    workspace_id: String,
    created_at: String,
    display_name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    ticket: Option<toml::Value>,
}

impl WorkspaceIdentity {
    pub fn load_or_init(workspace_root: impl AsRef<Path>) -> Result<Self> {
        Self::load_or_init_with_clock(workspace_root.as_ref(), || {
            Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
        })
    }

    pub fn load_required(workspace_root: impl AsRef<Path>) -> Result<Self> {
        let path = Self::path(workspace_root.as_ref());
        match fs::read_to_string(&path) {
            Ok(raw) => Self::parse_str(&raw, &path),
            Err(error) if error.kind() == ErrorKind::NotFound => {
                Err(Error::WorkspaceIdentity(format!(
                    "workspace is not initialized at {}; run `yoi-server init --workspace {}` first",
                    workspace_root.as_ref().display(),
                    workspace_root.as_ref().display()
                )))
            }
            Err(error) => Err(Error::Io(error)),
        }
    }

    pub fn path(workspace_root: impl AsRef<Path>) -> PathBuf {
        workspace_root
            .as_ref()
            .join(WORKSPACE_IDENTITY_RELATIVE_PATH)
    }

    pub fn parse_str(raw: &str, path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref();
        let parsed: WorkspaceIdentityFile = toml::from_str(raw).map_err(|error| {
            workspace_identity_error(path, format!("failed to parse TOML: {error}"))
        })?;
        Self::from_file(parsed, path)
    }

    fn load_or_init_with_clock(
        workspace_root: &Path,
        now_utc_rfc3339: impl FnOnce() -> String,
    ) -> Result<Self> {
        let path = Self::path(workspace_root);
        match fs::read_to_string(&path) {
            Ok(raw) => Self::parse_str(&raw, &path),
            Err(error) if error.kind() == ErrorKind::NotFound => {
                Self::init(workspace_root, &path, now_utc_rfc3339())
            }
            Err(error) => Err(Error::Io(error)),
        }
    }

    fn init(workspace_root: &Path, path: &Path, created_at: String) -> Result<Self> {
        validate_created_at(&created_at, path)?;
        let display_name = workspace_display_name_from_root(workspace_root, path)?;
        let workspace_id = Uuid::now_v7().to_string();
        let identity = Self {
            workspace_id,
            created_at,
            display_name,
        };
        identity.write_new_or_read_existing(path)
    }

    fn from_file(parsed: WorkspaceIdentityFile, path: &Path) -> Result<Self> {
        let workspace_id = validate_workspace_id(&parsed.workspace_id, path)?;
        validate_created_at(&parsed.created_at, path)?;
        validate_display_name(&parsed.display_name, path)?;
        Ok(Self {
            workspace_id,
            created_at: parsed.created_at,
            display_name: parsed.display_name,
        })
    }

    fn write_new_or_read_existing(&self, path: &Path) -> Result<Self> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let raw = toml::to_string_pretty(&WorkspaceIdentityFile {
            workspace_id: self.workspace_id.clone(),
            created_at: self.created_at.clone(),
            display_name: self.display_name.clone(),
            ticket: None,
        })
        .map_err(|error| {
            workspace_identity_error(path, format!("failed to encode TOML: {error}"))
        })?;

        match OpenOptions::new().write(true).create_new(true).open(path) {
            Ok(mut file) => {
                file.write_all(raw.as_bytes())?;
                file.sync_all()?;
                Ok(self.clone())
            }
            Err(error) if error.kind() == ErrorKind::AlreadyExists => {
                let raw = fs::read_to_string(path)?;
                Self::parse_str(&raw, path)
            }
            Err(error) => Err(Error::Io(error)),
        }
    }
}

fn validate_workspace_id(value: &str, path: &Path) -> Result<String> {
    let uuid = Uuid::parse_str(value).map_err(|error| {
        workspace_identity_error(path, format!("workspace_id is not a UUID: {error}"))
    })?;
    if uuid.get_version_num() != 7 {
        return Err(workspace_identity_error(
            path,
            "workspace_id must be a UUIDv7 canonical string".to_string(),
        ));
    }
    let canonical = uuid.to_string();
    if value != canonical {
        return Err(workspace_identity_error(
            path,
            "workspace_id must use lowercase hyphenated UUID canonical form".to_string(),
        ));
    }
    Ok(canonical)
}

fn validate_created_at(value: &str, path: &Path) -> Result<()> {
    let parsed = chrono::DateTime::parse_from_rfc3339(value).map_err(|error| {
        workspace_identity_error(path, format!("created_at is not RFC3339: {error}"))
    })?;
    if parsed.offset().local_minus_utc() != 0 || !value.ends_with('Z') {
        return Err(workspace_identity_error(
            path,
            "created_at must be a UTC RFC3339 timestamp ending in Z".to_string(),
        ));
    }
    Ok(())
}

fn validate_display_name(value: &str, path: &Path) -> Result<()> {
    if value.trim().is_empty() {
        return Err(workspace_identity_error(
            path,
            "display_name must not be empty".to_string(),
        ));
    }
    if value.contains('\0') || value.chars().any(|ch| ch.is_control()) {
        return Err(workspace_identity_error(
            path,
            "display_name must not contain control characters".to_string(),
        ));
    }
    Ok(())
}

fn workspace_display_name_from_root(workspace_root: &Path, path: &Path) -> Result<String> {
    let display_name = workspace_root
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or_else(|| {
            workspace_identity_error(
                path,
                "workspace root must have a UTF-8 final path component".to_string(),
            )
        })?
        .to_string();
    validate_display_name(&display_name, path)?;
    Ok(display_name)
}

fn workspace_identity_error(path: &Path, message: String) -> Error {
    Error::WorkspaceIdentity(format!("{}: {message}", path.display()))
}

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

    const FIXED_WORKSPACE_ID: &str = "0192f0e8-4d84-7d6e-a000-000000000001";
    const FIXED_CREATED_AT: &str = "2026-06-23T06:43:28Z";

    #[test]
    fn load_required_rejects_uninitialized_workspace_without_creating_identity() {
        let temp = tempfile::tempdir().unwrap();
        let workspace_root = temp.path().join("uninitialized-workspace");
        fs::create_dir_all(&workspace_root).unwrap();

        let error = WorkspaceIdentity::load_required(&workspace_root).unwrap_err();

        assert!(
            error.to_string().contains("workspace is not initialized"),
            "unexpected error: {error}"
        );
        assert!(!WorkspaceIdentity::path(&workspace_root).exists());
    }

    #[test]
    fn missing_identity_file_is_created_with_safe_fields() {
        let temp = tempfile::tempdir().unwrap();
        let workspace_root = temp.path().join("example-workspace");
        fs::create_dir_all(&workspace_root).unwrap();

        let identity = WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || {
            FIXED_CREATED_AT.to_string()
        })
        .unwrap();

        assert_eq!(identity.display_name, "example-workspace");
        assert_eq!(identity.created_at, FIXED_CREATED_AT);
        validate_workspace_id(
            &identity.workspace_id,
            &WorkspaceIdentity::path(&workspace_root),
        )
        .unwrap();

        let raw = fs::read_to_string(WorkspaceIdentity::path(&workspace_root)).unwrap();
        assert!(raw.contains("workspace_id"));
        assert!(raw.contains("display_name"));
        assert!(raw.contains("created_at"));
        assert!(!raw.contains(&workspace_root.to_string_lossy().to_string()));

        let reloaded = WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || {
            "2026-06-24T00:00:00Z".to_string()
        })
        .unwrap();
        assert_eq!(reloaded, identity);
    }

    #[test]
    fn existing_identity_file_is_stable() {
        let temp = tempfile::tempdir().unwrap();
        let workspace_root = temp.path().join("moved-workspace");
        let yoi_dir = workspace_root.join(".yoi");
        fs::create_dir_all(&yoi_dir).unwrap();
        let path = yoi_dir.join("workspace.toml");
        let raw = format!(
            "workspace_id = \"{FIXED_WORKSPACE_ID}\"\ncreated_at = \"{FIXED_CREATED_AT}\"\ndisplay_name = \"Stable Project\"\n"
        );
        fs::write(&path, &raw).unwrap();

        let identity = WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || {
            "2026-06-24T00:00:00Z".to_string()
        })
        .unwrap();

        assert_eq!(identity.workspace_id, FIXED_WORKSPACE_ID);
        assert_eq!(identity.created_at, FIXED_CREATED_AT);
        assert_eq!(identity.display_name, "Stable Project");
        assert_eq!(fs::read_to_string(path).unwrap(), raw);
    }

    #[test]
    fn create_new_race_returns_existing_persisted_identity() {
        let temp = tempfile::tempdir().unwrap();
        let path = temp.path().join(".yoi/workspace.toml");
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        let persisted_raw = format!(
            "workspace_id = \"{FIXED_WORKSPACE_ID}\"\ncreated_at = \"{FIXED_CREATED_AT}\"\ndisplay_name = \"Persisted Project\"\n"
        );
        fs::write(&path, &persisted_raw).unwrap();
        let generated = WorkspaceIdentity {
            workspace_id: "0192f0e8-4d84-7d6e-b000-000000000002".to_string(),
            created_at: "2026-06-24T00:00:00Z".to_string(),
            display_name: "Generated Project".to_string(),
        };

        let returned = generated.write_new_or_read_existing(&path).unwrap();

        assert_eq!(returned.workspace_id, FIXED_WORKSPACE_ID);
        assert_eq!(returned.created_at, FIXED_CREATED_AT);
        assert_eq!(returned.display_name, "Persisted Project");
        assert_eq!(fs::read_to_string(path).unwrap(), persisted_raw);
    }

    #[test]
    fn invalid_identity_file_fails_closed_without_rewriting() {
        let temp = tempfile::tempdir().unwrap();
        let workspace_root = temp.path().join("invalid-workspace");
        let yoi_dir = workspace_root.join(".yoi");
        fs::create_dir_all(&yoi_dir).unwrap();
        let path = yoi_dir.join("workspace.toml");
        let raw = "workspace_id = \"not-a-uuid\"\ncreated_at = \"2026-06-23T06:43:28Z\"\ndisplay_name = \"Invalid\"\n";
        fs::write(&path, raw).unwrap();

        let error = WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || {
            FIXED_CREATED_AT.to_string()
        })
        .unwrap_err();

        assert!(error.to_string().contains("workspace_id is not a UUID"));
        assert_eq!(fs::read_to_string(path).unwrap(), raw);
    }

    #[test]
    fn generated_identity_does_not_leak_parent_paths() {
        let temp = tempfile::tempdir().unwrap();
        let secret_parent = temp.path().join("user-secret-parent");
        let workspace_root = secret_parent.join("public-project-name");
        fs::create_dir_all(&workspace_root).unwrap();

        WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || {
            FIXED_CREATED_AT.to_string()
        })
        .unwrap();
        let raw = fs::read_to_string(WorkspaceIdentity::path(&workspace_root)).unwrap();

        assert!(raw.contains("public-project-name"));
        assert!(!raw.contains(&secret_parent.to_string_lossy().to_string()));
        assert!(!raw.contains("user-secret-parent"));
        assert!(!raw.contains("/"));
    }

    #[test]
    fn unknown_fields_are_rejected() {
        let temp = tempfile::tempdir().unwrap();
        let path = temp.path().join("workspace.toml");
        let raw = format!(
            "workspace_id = \"{FIXED_WORKSPACE_ID}\"\ncreated_at = \"{FIXED_CREATED_AT}\"\ndisplay_name = \"Stable Project\"\nlocal_root = \"/tmp/secret\"\n"
        );

        let error = WorkspaceIdentity::parse_str(&raw, &path).unwrap_err();

        assert!(error.to_string().contains("unknown field"));
    }
}
