use serde::{Deserialize, Serialize};
use serde_json::Value;

pub const EVIDENCE_LOCATOR_SCHEMA: &str = "mnote.evidence_locator.v1";
pub const RESOURCE_SOURCE_MAP_SCHEMA: &str = "mnote.resource_source_map.v1";
pub const PARSED_RESOURCE_ARTIFACT_SCHEMA: &str = "mnote.parsed_resource_artifact.v1";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceResourceKind {
    Markdown,
    Pdf,
    Image,
    Office,
    Mindmap,
    RawFile,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceBBox {
    pub x0: f64,
    pub y0: f64,
    pub x1: f64,
    pub y1: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceRange {
    pub start: u64,
    pub end: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceOpenAction {
    pub action_type: String,
    pub url: String,
    #[serde(default, skip_serializing_if = "Value::is_null")]
    pub params: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceLocator {
    pub schema: String,
    pub root_uri: String,
    pub owner_document_id: String,
    pub owner_document_path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resource_path: Option<String>,
    pub resource_kind: EvidenceResourceKind,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bbox: Option<EvidenceBBox>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub section_path: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub line_range: Option<EvidenceRange>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub char_range: Option<EvidenceRange>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub block_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_map_path: Option<String>,
    pub open_action: EvidenceOpenAction,
}

impl EvidenceLocator {
    pub fn new(
        root_uri: impl Into<String>,
        owner_document_id: impl Into<String>,
        owner_document_path: impl Into<String>,
        resource_kind: EvidenceResourceKind,
        open_action: EvidenceOpenAction,
    ) -> Self {
        Self {
            schema: EVIDENCE_LOCATOR_SCHEMA.into(),
            root_uri: root_uri.into(),
            owner_document_id: owner_document_id.into(),
            owner_document_path: owner_document_path.into(),
            resource_path: None,
            resource_kind,
            page: None,
            bbox: None,
            section_path: Vec::new(),
            line_range: None,
            char_range: None,
            block_id: None,
            source_map_path: None,
            open_action,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SourceMapTextItem {
    pub id: String,
    pub text: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bbox: Option<EvidenceBBox>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub char_range: Option<EvidenceRange>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SourceMapBlockKind {
    Text,
    Heading,
    Paragraph,
    Table,
    Figure,
    Image,
    List,
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SourceMapBlock {
    pub id: String,
    pub block_type: SourceMapBlockKind,
    pub text: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bbox: Option<EvidenceBBox>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub char_range: Option<EvidenceRange>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SourceMapPage {
    pub page: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<f64>,
    #[serde(default)]
    pub text_items: Vec<SourceMapTextItem>,
    #[serde(default)]
    pub blocks: Vec<SourceMapBlock>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SourceMapSection {
    pub id: String,
    pub title: String,
    #[serde(default)]
    pub path: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page_start: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page_end: Option<u32>,
    #[serde(default)]
    pub block_ids: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ResourceSourceMap {
    pub schema: String,
    pub provider: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model_version: Option<String>,
    pub owner_document_path: String,
    pub source_root_relative_path: String,
    pub source_hash: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page_count: Option<u32>,
    #[serde(default)]
    pub pages: Vec<SourceMapPage>,
    #[serde(default)]
    pub sections: Vec<SourceMapSection>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ParsedResourceArtifact {
    pub schema: String,
    pub provider: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model_version: Option<String>,
    pub owner_document_id: String,
    pub owner_document_path: String,
    pub source_root_relative_path: String,
    pub source_hash: String,
    pub artifact_root_relative_path: String,
    pub source_map_root_relative_path: String,
    pub updated_at_ms: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceSearchMode {
    Keyword,
    Tree,
    Hybrid,
    Graph,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceSearchScope {
    pub workspace_id: String,
    pub root_uri: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_document_id: Option<String>,
    #[serde(default)]
    pub include_resources: bool,
    #[serde(default)]
    pub include_ocr: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceSearchRequest {
    pub query: String,
    pub scope: EvidenceSearchScope,
    #[serde(default = "default_evidence_search_mode")]
    pub mode: EvidenceSearchMode,
    #[serde(default = "default_evidence_top_k")]
    pub top_k: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceSearchResult {
    pub evidence_id: String,
    pub quote: String,
    pub score: f64,
    pub source: EvidenceLocator,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub match_info: Option<EvidenceSearchMatchInfo>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub citation_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub citation_label: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub citation_markdown: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceSearchMatchInfo {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rank: Option<u32>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub matched_terms: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub missing_terms: Vec<String>,
    pub match_mode: String,
    pub match_scope: String,
    pub strong: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceSearchResponse {
    pub ok: bool,
    #[serde(default)]
    pub results: Vec<EvidenceSearchResult>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub diagnostics: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceReadContext {
    pub before_blocks: u32,
    pub after_blocks: u32,
    pub include_section_summary: bool,
}

impl Default for EvidenceReadContext {
    fn default() -> Self {
        Self {
            before_blocks: 3,
            after_blocks: 3,
            include_section_summary: true,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceReadRequest {
    pub locator: EvidenceLocator,
    #[serde(default)]
    pub context: EvidenceReadContext,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceReadResponse {
    pub ok: bool,
    pub locator: EvidenceLocator,
    pub quote: String,
    #[serde(default)]
    pub context_blocks: Vec<EvidenceSearchResult>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceOpenRequest {
    pub locator: EvidenceLocator,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EvidenceEdge {
    pub edge_id: String,
    pub from_id: String,
    pub to_id: String,
    pub edge_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_evidence_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_locator: Option<EvidenceLocator>,
    pub confidence: f64,
    pub created_by: String,
}

fn default_evidence_search_mode() -> EvidenceSearchMode {
    EvidenceSearchMode::Hybrid
}

fn default_evidence_top_k() -> u32 {
    8
}

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

    #[test]
    fn evidence_locator_serializes_canonical_schema() {
        let locator = EvidenceLocator::new(
            "file:///workspace",
            "local-md:docs~2FPage.md",
            "docs/Page.md",
            EvidenceResourceKind::Pdf,
            EvidenceOpenAction {
                action_type: "mnote.open_resource_locator".into(),
                url: "/documents/local-md:docs~2FPage.md?resource=docs%2Fspec.pdf".into(),
                params: Value::Null,
            },
        );

        let value = serde_json::to_value(locator).expect("locator should serialize");
        assert_eq!(value["schema"], EVIDENCE_LOCATOR_SCHEMA);
        assert_eq!(value["resourceKind"], "pdf");
        assert!(value.get("bbox").is_none());
        assert!(value["openAction"]["params"].is_null());
    }

    #[test]
    fn source_map_keeps_single_provider_contract() {
        let source_map = ResourceSourceMap {
            schema: RESOURCE_SOURCE_MAP_SCHEMA.into(),
            provider: "liteparse".into(),
            model_version: Some("2.0.5".into()),
            owner_document_path: "docs/Page.md".into(),
            source_root_relative_path: "docs/Page.assets/spec.pdf".into(),
            source_hash: "sha256:demo".into(),
            page_count: Some(1),
            pages: vec![SourceMapPage {
                page: 1,
                width: Some(595.0),
                height: Some(842.0),
                text_items: vec![SourceMapTextItem {
                    id: "p1_t1".into(),
                    text: "Revenue".into(),
                    bbox: Some(EvidenceBBox {
                        x0: 72.0,
                        y0: 124.0,
                        x1: 160.0,
                        y1: 140.0,
                    }),
                    char_range: Some(EvidenceRange { start: 0, end: 7 }),
                }],
                blocks: vec![],
            }],
            sections: vec![],
        };

        let value = serde_json::to_value(source_map).expect("source map should serialize");
        assert_eq!(value["schema"], RESOURCE_SOURCE_MAP_SCHEMA);
        assert_eq!(value["pages"][0]["textItems"][0]["bbox"]["x0"], 72.0);
    }
}
