//! Filesystem sprint store.
//!
//! Project-scoped sprints live under `<root>/.ideai/sprints/`, one directory per
//! stable sprint id. The Markdown files are the source of truth; `index.json` is
//! a reconstructible projection kept in sync after writes.

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

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use domain::{
    IssueActor, ProjectPath, Sprint, SprintId, SprintIndexEntry, SprintOrder, SprintStatus,
    SprintStore, SprintStoreError, SprintVersion,
};

const IDEAI_DIR: &str = ".ideai";
const SPRINTS_DIR: &str = "sprints";
const SPRINT_FILE: &str = "sprint.md";
const INDEX_FILE: &str = "index.json";
const INDEX_VERSION: u32 = 1;

/// Filesystem-backed sprint store.
#[derive(Debug, Clone, Default)]
pub struct FsSprintStore;

impl FsSprintStore {
    /// Builds a store.
    #[must_use]
    pub const fn new() -> Self {
        Self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct IndexDoc {
    version: u32,
    sprints: Vec<SprintIndexEntry>,
}

fn sprint_root(root: &ProjectPath) -> PathBuf {
    PathBuf::from(root.as_str())
        .join(IDEAI_DIR)
        .join(SPRINTS_DIR)
}

fn sprint_dir(root: &ProjectPath, sprint_id: SprintId) -> PathBuf {
    sprint_root(root).join(sprint_id.to_string())
}

fn sprint_path(root: &ProjectPath, sprint_id: SprintId) -> PathBuf {
    sprint_dir(root, sprint_id).join(SPRINT_FILE)
}

fn index_path(root: &ProjectPath) -> PathBuf {
    sprint_root(root).join(INDEX_FILE)
}

async fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), SprintStoreError> {
    let parent = path
        .parent()
        .ok_or_else(|| SprintStoreError::Store("path has no parent".to_owned()))?;
    tokio::fs::create_dir_all(parent).await.map_err(io_error)?;
    let tmp = path.with_extension("tmp");
    tokio::fs::write(&tmp, bytes).await.map_err(io_error)?;
    tokio::fs::rename(&tmp, path).await.map_err(io_error)
}

async fn read_string(path: &Path) -> Result<String, SprintStoreError> {
    let bytes = tokio::fs::read(path).await.map_err(|err| {
        if err.kind() == std::io::ErrorKind::NotFound {
            SprintStoreError::NotFound
        } else {
            io_error(err)
        }
    })?;
    String::from_utf8(bytes).map_err(|err| SprintStoreError::Store(err.to_string()))
}

fn io_error(err: std::io::Error) -> SprintStoreError {
    SprintStoreError::Store(err.to_string())
}

async fn load_sprint(root: &ProjectPath, sprint_id: SprintId) -> Result<Sprint, SprintStoreError> {
    let text = read_string(&sprint_path(root, sprint_id)).await?;
    parse_sprint_doc(&text)
}

async fn save_sprint(root: &ProjectPath, sprint: &Sprint) -> Result<(), SprintStoreError> {
    let dir = sprint_dir(root, sprint.id);
    tokio::fs::create_dir_all(&dir).await.map_err(io_error)?;
    write_atomic(&dir.join(SPRINT_FILE), render_sprint_doc(sprint).as_bytes()).await
}

async fn rebuild_index(root: &ProjectPath) -> Result<Vec<SprintIndexEntry>, SprintStoreError> {
    let dir = sprint_root(root);
    let mut rows = Vec::new();
    let mut entries = match tokio::fs::read_dir(&dir).await {
        Ok(entries) => entries,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
            write_index(root, &rows).await?;
            return Ok(rows);
        }
        Err(err) => return Err(io_error(err)),
    };
    while let Some(entry) = entries.next_entry().await.map_err(io_error)? {
        let Ok(file_type) = entry.file_type().await else {
            continue;
        };
        if !file_type.is_dir() {
            continue;
        }
        let name = entry.file_name().to_string_lossy().to_string();
        let Ok(id) = uuid::Uuid::parse_str(&name) else {
            continue;
        };
        if let Ok(sprint) = load_sprint(root, SprintId::from_uuid(id)).await {
            rows.push(SprintIndexEntry::from(&sprint));
        }
    }
    rows.sort_by_key(|row| row.order.get());
    write_index(root, &rows).await?;
    Ok(rows)
}

async fn read_index(root: &ProjectPath) -> Result<Vec<SprintIndexEntry>, SprintStoreError> {
    match tokio::fs::read(index_path(root)).await {
        Ok(bytes) => {
            let doc: IndexDoc = serde_json::from_slice(&bytes)
                .map_err(|err| SprintStoreError::Store(err.to_string()))?;
            Ok(doc.sprints)
        }
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => rebuild_index(root).await,
        Err(err) => Err(io_error(err)),
    }
}

async fn write_index(
    root: &ProjectPath,
    rows: &[SprintIndexEntry],
) -> Result<(), SprintStoreError> {
    let doc = IndexDoc {
        version: INDEX_VERSION,
        sprints: rows.to_vec(),
    };
    let bytes =
        serde_json::to_vec_pretty(&doc).map_err(|err| SprintStoreError::Store(err.to_string()))?;
    write_atomic(&index_path(root), &bytes).await
}

#[async_trait]
impl SprintStore for FsSprintStore {
    async fn create(&self, root: &ProjectPath, sprint: &Sprint) -> Result<(), SprintStoreError> {
        sprint
            .validate()
            .map_err(|err| SprintStoreError::Invalid(err.to_string()))?;
        if tokio::fs::try_exists(sprint_path(root, sprint.id))
            .await
            .map_err(io_error)?
        {
            return Err(SprintStoreError::Invalid(format!(
                "sprint {} already exists",
                sprint.id
            )));
        }
        save_sprint(root, sprint).await?;
        rebuild_index(root).await?;
        Ok(())
    }

    async fn get(
        &self,
        root: &ProjectPath,
        sprint_id: SprintId,
    ) -> Result<Sprint, SprintStoreError> {
        load_sprint(root, sprint_id).await
    }

    async fn list(&self, root: &ProjectPath) -> Result<Vec<SprintIndexEntry>, SprintStoreError> {
        let mut rows = read_index(root).await?;
        rows.sort_by_key(|row| row.order.get());
        Ok(rows)
    }

    async fn update(
        &self,
        root: &ProjectPath,
        sprint: &Sprint,
        expected_version: SprintVersion,
    ) -> Result<(), SprintStoreError> {
        sprint
            .validate()
            .map_err(|err| SprintStoreError::Invalid(err.to_string()))?;
        let current = load_sprint(root, sprint.id).await?;
        if current.version != expected_version {
            return Err(SprintStoreError::VersionConflict {
                expected: expected_version,
                actual: current.version,
            });
        }
        save_sprint(root, sprint).await?;
        rebuild_index(root).await?;
        Ok(())
    }

    async fn delete(
        &self,
        root: &ProjectPath,
        sprint_id: SprintId,
    ) -> Result<(), SprintStoreError> {
        if !tokio::fs::try_exists(sprint_path(root, sprint_id))
            .await
            .map_err(io_error)?
        {
            return Err(SprintStoreError::NotFound);
        }
        tokio::fs::remove_dir_all(sprint_dir(root, sprint_id))
            .await
            .map_err(io_error)?;
        rebuild_index(root).await?;
        Ok(())
    }
}

fn render_sprint_doc(sprint: &Sprint) -> String {
    format!(
        "---\n\
id: {}\n\
order: {}\n\
name: {}\n\
status: {}\n\
createdBy: {}\n\
updatedBy: {}\n\
createdAt: {}\n\
updatedAt: {}\n\
version: {}\n\
---\n",
        json_string(&sprint.id.to_string()),
        sprint.order.get(),
        json_string(&sprint.name),
        json_value(&sprint.status),
        json_value(&sprint.created_by),
        json_value(&sprint.updated_by),
        sprint.created_at,
        sprint.updated_at,
        sprint.version.get(),
    )
}

fn parse_sprint_doc(text: &str) -> Result<Sprint, SprintStoreError> {
    let (fm, _body) = split_frontmatter(text)?;
    let map = parse_frontmatter_map(fm)?;
    let id_raw: String = parse_json_field(&map, "id")?;
    let id = SprintId::from_uuid(
        uuid::Uuid::parse_str(&id_raw).map_err(|err| SprintStoreError::Invalid(err.to_string()))?,
    );
    let order = SprintOrder::new(parse_plain_u32(&map, "order")?)
        .map_err(|err| SprintStoreError::Invalid(err.to_string()))?;
    let name: String = parse_json_field(&map, "name")?;
    let status: SprintStatus = parse_json_field(&map, "status")?;
    let created_by: IssueActor = parse_json_field(&map, "createdBy")?;
    let updated_by: IssueActor = parse_json_field(&map, "updatedBy")?;
    let created_at = parse_plain_u64(&map, "createdAt")?;
    let updated_at = parse_plain_u64(&map, "updatedAt")?;
    let version = SprintVersion::new(parse_plain_u64(&map, "version")?)
        .map_err(|err| SprintStoreError::Invalid(err.to_string()))?;
    Sprint::rehydrate(Sprint {
        id,
        order,
        name,
        status,
        created_by,
        updated_by,
        created_at,
        updated_at,
        version,
    })
    .map_err(|err| SprintStoreError::Invalid(err.to_string()))
}

fn split_frontmatter(text: &str) -> Result<(&str, &str), SprintStoreError> {
    let rest = text
        .strip_prefix("---\n")
        .or_else(|| text.strip_prefix("---\r\n"))
        .ok_or_else(|| SprintStoreError::Invalid("missing opening frontmatter fence".to_owned()))?;
    let mut offset = 0;
    for line in rest.split_inclusive('\n') {
        if line.trim_end_matches(['\r', '\n']) == "---" {
            let body_start = offset + line.len();
            return Ok((&rest[..offset], &rest[body_start..]));
        }
        offset += line.len();
    }
    Err(SprintStoreError::Invalid(
        "missing closing frontmatter fence".to_owned(),
    ))
}

fn parse_frontmatter_map(block: &str) -> Result<BTreeMap<String, String>, SprintStoreError> {
    let mut map = BTreeMap::new();
    for line in block.lines().filter(|line| !line.trim().is_empty()) {
        let (key, value) = line
            .split_once(':')
            .ok_or_else(|| SprintStoreError::Invalid("frontmatter line missing ':'".to_owned()))?;
        map.insert(key.trim().to_owned(), value.trim().to_owned());
    }
    Ok(map)
}

fn parse_plain_u32(map: &BTreeMap<String, String>, key: &str) -> Result<u32, SprintStoreError> {
    map.get(key)
        .ok_or_else(|| SprintStoreError::Invalid(format!("missing frontmatter key `{key}`")))?
        .parse::<u32>()
        .map_err(|err| SprintStoreError::Invalid(err.to_string()))
}

fn parse_plain_u64(map: &BTreeMap<String, String>, key: &str) -> Result<u64, SprintStoreError> {
    map.get(key)
        .ok_or_else(|| SprintStoreError::Invalid(format!("missing frontmatter key `{key}`")))?
        .parse::<u64>()
        .map_err(|err| SprintStoreError::Invalid(err.to_string()))
}

fn parse_json_field<T: serde::de::DeserializeOwned>(
    map: &BTreeMap<String, String>,
    key: &str,
) -> Result<T, SprintStoreError> {
    let value = map
        .get(key)
        .ok_or_else(|| SprintStoreError::Invalid(format!("missing frontmatter key `{key}`")))?;
    serde_json::from_str(value).map_err(|err| SprintStoreError::Invalid(err.to_string()))
}

fn json_value<T: Serialize>(value: &T) -> String {
    serde_json::to_string(value).expect("serializing sprint frontmatter cannot fail")
}

fn json_string(value: &str) -> String {
    serde_json::to_string(value).expect("serializing sprint string cannot fail")
}
