use crate::error::WebError;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::path::{Path, PathBuf};

const OCR_INDEX_VERSION: u32 = 1;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct OcrIndex {
    version: u32,
    #[serde(default)]
    entries: BTreeMap<String, OcrIndexEntry>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct OcrIndexEntry {
    pub(crate) job_id: String,
    pub(crate) owner_document_id: String,
    pub(crate) owner_document_path: String,
    pub(crate) source_root_relative_path: String,
    pub(crate) ocr_root_relative_path: String,
    pub(crate) provider: String,
    pub(crate) model_version: String,
    pub(crate) status: String,
    pub(crate) source_size: u64,
    pub(crate) source_mtime_ms: u128,
    pub(crate) created_at_ms: u128,
    pub(crate) updated_at_ms: u128,
    #[serde(default)]
    pub(crate) plain_text_preview: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(crate) error: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct OcrFrontmatter {
    pub(crate) provider: String,
    pub(crate) owner_document: String,
    pub(crate) source_path: String,
    pub(crate) source_root_relative_path: String,
    pub(crate) source_size: u64,
    pub(crate) source_mtime_ms: u128,
    pub(crate) status: String,
}

pub(crate) fn is_local_ocr_sidecar_relative_path(relative_path: &str) -> bool {
    let normalized = relative_path.trim().replace('\\', "/");
    let path = Path::new(&normalized);
    let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
        return false;
    };
    let Some(parent_name) = path
        .parent()
        .and_then(|value| value.file_name())
        .and_then(|value| value.to_str())
    else {
        return false;
    };
    parent_name.ends_with(".ocr") && file_name.ends_with(".ocr.md")
}

pub(crate) fn parse_ocr_frontmatter(markdown: &str) -> Option<OcrFrontmatter> {
    let trimmed = markdown.strip_prefix("---\n")?;
    let end = trimmed.find("\n---")?;
    let frontmatter = &trimmed[..end];
    let mut fields = HashMap::<String, String>::new();
    for line in frontmatter.lines() {
        let Some((key, value)) = line.split_once(':') else {
            continue;
        };
        fields.insert(
            key.trim().to_string(),
            value.trim().trim_matches('"').to_string(),
        );
    }
    if fields.get("mnote_ocr_version").map(String::as_str) != Some("1") {
        return None;
    }
    Some(OcrFrontmatter {
        provider: fields.get("provider")?.to_string(),
        owner_document: fields.get("owner_document")?.to_string(),
        source_path: fields.get("source_path")?.to_string(),
        source_root_relative_path: fields.get("source_root_relative_path")?.to_string(),
        source_size: fields.get("source_size")?.parse().ok()?,
        source_mtime_ms: fields.get("source_mtime_ms")?.parse().ok()?,
        status: fields.get("status")?.to_string(),
    })
}

pub(crate) fn strip_ocr_frontmatter(markdown: &str) -> &str {
    let Some(trimmed) = markdown.strip_prefix("---\n") else {
        return markdown;
    };
    let Some(end) = trimmed.find("\n---") else {
        return markdown;
    };
    trimmed[end + "\n---".len()..].trim_start_matches('\n')
}

fn read_ocr_index(root: &Path) -> Result<OcrIndex, WebError> {
    let path = ocr_index_path(root);
    if !path.exists() {
        return Ok(OcrIndex {
            version: OCR_INDEX_VERSION,
            entries: BTreeMap::new(),
        });
    }
    let content = fs::read_to_string(&path).map_err(|error| {
        WebError::bad_request_code(
            "local_ocr_index_read_failed",
            format!("无法读取历史 OCR 索引 {}: {error}", path.display()),
        )
    })?;
    serde_json::from_str::<OcrIndex>(&content).map_err(|error| {
        WebError::bad_request_code(
            "local_ocr_index_invalid",
            format!("历史 OCR 索引格式非法 {}: {error}", path.display()),
        )
    })
}

fn ocr_index_path(root: &Path) -> PathBuf {
    root.join(".mnote").join("ocr-index.json")
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use std::time::{SystemTime, UNIX_EPOCH};

    fn temp_root(name: &str) -> PathBuf {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|value| value.as_nanos())
            .unwrap_or_default();
        let root = std::env::temp_dir().join(format!("{name}-{nonce}"));
        fs::create_dir_all(&root).expect("temp root");
        root
    }

    #[test]
    fn local_ocr_sidecar_path_and_frontmatter_contract() {
        assert!(is_local_ocr_sidecar_relative_path(
            "docs/Page.ocr/photo.png.ocr.md"
        ));
        assert!(is_local_ocr_sidecar_relative_path(
            "docs/Page.ocr/photo.png-704905.ocr.md"
        ));
        assert!(!is_local_ocr_sidecar_relative_path(
            "docs/Page.assets/photo.png"
        ));

        let markdown = "---\nmnote_ocr_version: 1\nprovider: mock\nowner_document: ../Page.md\nsource_path: ./Page.assets/photo.png\nsource_root_relative_path: docs/Page.assets/photo.png\nsource_size: 3\nsource_mtime_ms: 1\nstatus: done\n---\n\nOCR body\n";
        let parsed = parse_ocr_frontmatter(markdown).expect("frontmatter");
        assert_eq!(parsed.provider, "mock");
        assert_eq!(
            parsed.source_root_relative_path,
            "docs/Page.assets/photo.png"
        );
        assert_eq!(strip_ocr_frontmatter(markdown), "OCR body\n");
    }

    #[test]
    fn local_ocr_index_reads_historical_entries_only() {
        let root = temp_root("mnote-local-ocr-index-retired");
        fs::create_dir_all(root.join(".mnote")).expect("mnote dir");
        fs::write(
            ocr_index_path(&root),
            serde_json::to_string_pretty(&json!({
                "version": 1,
                "entries": {
                    "docs/Page.assets/photo.png": {
                        "jobId": "ocr_test",
                        "ownerDocumentId": "local-md:docs~2FPage.md",
                        "ownerDocumentPath": "docs/Page.md",
                        "sourceRootRelativePath": "docs/Page.assets/photo.png",
                        "ocrRootRelativePath": "docs/Page.ocr/photo.png.ocr.md",
                        "provider": "mock",
                        "modelVersion": "vlm",
                        "status": "done",
                        "sourceSize": 3,
                        "sourceMtimeMs": 1,
                        "createdAtMs": 1,
                        "updatedAtMs": 1,
                        "plainTextPreview": "OCR body"
                    }
                }
            }))
            .expect("serialize index"),
        )
        .expect("ocr index");

        let index = read_ocr_index(&root).expect("read historical index");
        let entry = index
            .entries
            .get("docs/Page.assets/photo.png")
            .expect("historical entry");
        assert_eq!(entry.status, "done");
        assert_eq!(entry.provider, "mock");
        let _ = fs::remove_dir_all(root);
    }
}
