use std::fs;
use std::path::{Path, PathBuf};

use project_record::validate_record_id;
use serde::{Deserialize, Serialize};
use ticket::{LocalTicketBackend, TicketFilter, TicketIdOrSlug};

use crate::{Error, Result};

const DETAIL_BODY_LIMIT: usize = 64 * 1024;
const SUMMARY_BODY_LIMIT: usize = 240;

#[derive(Debug, Clone)]
pub struct LocalProjectRecordReader {
    workspace_root: PathBuf,
    ticket_backend: LocalTicketBackend,
}

impl LocalProjectRecordReader {
    pub fn new(workspace_root: impl Into<PathBuf>) -> Self {
        let workspace_root = workspace_root.into();
        let ticket_root = workspace_root.join(".yoi/tickets");
        Self {
            workspace_root,
            ticket_backend: LocalTicketBackend::new(ticket_root),
        }
    }

    pub fn workspace_root(&self) -> &Path {
        self.workspace_root.as_path()
    }

    pub fn list_tickets(&self, limit: usize) -> Result<ProjectRecordList<TicketSummary>> {
        let partial = self.ticket_backend.list_partial(TicketFilter::all())?;
        let mut items = partial
            .tickets
            .into_iter()
            .map(|item| TicketSummary {
                id: item.id,
                title: item.title,
                state: item.workflow_state.as_str().to_string(),
                priority: item.priority,
                updated_at: item.updated_at,
                queued_by: item.queued_by,
                queued_at: item.queued_at,
                record_source: "local_yoi_ticket".to_string(),
            })
            .collect::<Vec<_>>();
        items.sort_by(|a, b| {
            b.updated_at
                .cmp(&a.updated_at)
                .then_with(|| a.id.cmp(&b.id))
        });
        items.truncate(limit.min(200));
        Ok(ProjectRecordList {
            items,
            invalid_records: partial
                .invalid_records
                .into_iter()
                .map(|record| InvalidProjectRecord {
                    label: record.label,
                    reason: record.reason,
                })
                .collect(),
            record_authority: "local_yoi_project_records".to_string(),
        })
    }

    pub fn ticket(&self, id: &str) -> Result<TicketDetail> {
        validate_project_id(id)?;
        let partial = self
            .ticket_backend
            .show_partial(TicketIdOrSlug::Id(id.to_string()))?;
        let ticket = partial.ticket;
        let (body, body_truncated) =
            truncate_body(ticket.document.body.as_str(), DETAIL_BODY_LIMIT);
        Ok(TicketDetail {
            id: ticket.meta.id,
            title: ticket.meta.title,
            state: ticket.meta.workflow_state.as_str().to_string(),
            priority: ticket.meta.priority,
            created_at: ticket.meta.created_at,
            updated_at: ticket.meta.updated_at,
            queued_by: ticket.meta.queued_by,
            queued_at: ticket.meta.queued_at,
            risk_flags: ticket.meta.risk_flags,
            body,
            body_truncated,
            event_count: ticket.events.len(),
            artifact_count: ticket.artifacts.len(),
            record_source: "local_yoi_ticket".to_string(),
        })
    }

    pub fn list_objectives(&self, limit: usize) -> Result<ProjectRecordList<ObjectiveSummary>> {
        let mut items = Vec::new();
        let mut invalid_records = Vec::new();
        let root = self.workspace_root.join(".yoi/objectives");
        if !root.exists() {
            return Ok(ProjectRecordList {
                items,
                invalid_records,
                record_authority: "local_yoi_project_records".to_string(),
            });
        }

        for entry in fs::read_dir(&root)? {
            let entry = entry?;
            let path = entry.path();
            if !path.is_dir() {
                continue;
            }
            let id = entry.file_name().to_string_lossy().to_string();
            match read_objective_summary(&path, &id) {
                Ok(item) => items.push(item),
                Err(error) => invalid_records.push(InvalidProjectRecord {
                    label: id,
                    reason: error.to_string(),
                }),
            }
        }
        items.sort_by(|a, b| {
            b.updated_at
                .cmp(&a.updated_at)
                .then_with(|| a.id.cmp(&b.id))
        });
        items.truncate(limit.min(200));
        Ok(ProjectRecordList {
            items,
            invalid_records,
            record_authority: "local_yoi_project_records".to_string(),
        })
    }

    pub fn objective(&self, id: &str) -> Result<ObjectiveDetail> {
        validate_project_id(id)?;
        let path = self.workspace_root.join(".yoi/objectives").join(id);
        let raw = fs::read_to_string(path.join("item.md"))?;
        let (frontmatter, body) = split_frontmatter(&raw, id)?;
        let meta: ObjectiveFrontmatter = serde_yaml::from_str(frontmatter)?;
        let (body, body_truncated) = truncate_body(body, DETAIL_BODY_LIMIT);
        Ok(ObjectiveDetail {
            id: id.to_string(),
            title: meta.title,
            state: meta.state,
            created_at: meta.created_at,
            updated_at: meta.updated_at,
            linked_tickets: meta.linked_tickets,
            body,
            body_truncated,
            record_source: "local_yoi_objective".to_string(),
        })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProjectRecordList<T> {
    pub items: Vec<T>,
    pub invalid_records: Vec<InvalidProjectRecord>,
    pub record_authority: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InvalidProjectRecord {
    pub label: String,
    pub reason: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketSummary {
    pub id: String,
    pub title: String,
    pub state: String,
    pub priority: String,
    pub updated_at: Option<String>,
    pub queued_by: Option<String>,
    pub queued_at: Option<String>,
    pub record_source: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TicketDetail {
    pub id: String,
    pub title: String,
    pub state: String,
    pub priority: String,
    pub created_at: Option<String>,
    pub updated_at: Option<String>,
    pub queued_by: Option<String>,
    pub queued_at: Option<String>,
    pub risk_flags: Vec<String>,
    pub body: String,
    pub body_truncated: bool,
    pub event_count: usize,
    pub artifact_count: usize,
    pub record_source: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectiveSummary {
    pub id: String,
    pub title: String,
    pub state: String,
    pub updated_at: Option<String>,
    pub summary: String,
    pub linked_tickets: Vec<String>,
    pub record_source: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectiveDetail {
    pub id: String,
    pub title: String,
    pub state: String,
    pub created_at: Option<String>,
    pub updated_at: Option<String>,
    pub linked_tickets: Vec<String>,
    pub body: String,
    pub body_truncated: bool,
    pub record_source: String,
}

#[derive(Debug, Deserialize)]
struct ObjectiveFrontmatter {
    title: String,
    state: String,
    #[serde(default)]
    created_at: Option<String>,
    #[serde(default)]
    updated_at: Option<String>,
    #[serde(default)]
    linked_tickets: Vec<String>,
}

fn read_objective_summary(path: &Path, id: &str) -> Result<ObjectiveSummary> {
    validate_project_id(id)?;
    let raw = fs::read_to_string(path.join("item.md"))?;
    let (frontmatter, body) = split_frontmatter(&raw, id)?;
    let meta: ObjectiveFrontmatter = serde_yaml::from_str(frontmatter)?;
    Ok(ObjectiveSummary {
        id: id.to_string(),
        title: meta.title,
        state: meta.state,
        updated_at: meta.updated_at,
        summary: summarize_body(body),
        linked_tickets: meta.linked_tickets,
        record_source: "local_yoi_objective".to_string(),
    })
}

fn split_frontmatter<'a>(raw: &'a str, label: &str) -> Result<(&'a str, &'a str)> {
    let rest = raw
        .strip_prefix("---\n")
        .ok_or_else(|| Error::MissingFrontmatter(label.to_string()))?;
    let Some((frontmatter, body)) = rest.split_once("\n---\n") else {
        return Err(Error::MissingFrontmatter(label.to_string()));
    };
    Ok((frontmatter, body))
}

fn validate_project_id(id: &str) -> Result<()> {
    validate_record_id(id).map_err(|_| Error::InvalidRecordId(id.to_string()))
}

fn summarize_body(body: &str) -> String {
    let summary = body
        .lines()
        .map(str::trim)
        .find(|line| !line.is_empty() && !line.starts_with('#'))
        .unwrap_or_default();
    let (summary, truncated) = truncate_body(summary, SUMMARY_BODY_LIMIT);
    if truncated {
        format!("{summary}…")
    } else {
        summary
    }
}

fn truncate_body(body: &str, limit: usize) -> (String, bool) {
    if body.len() <= limit {
        return (body.to_string(), false);
    }
    let mut end = limit;
    while !body.is_char_boundary(end) {
        end -= 1;
    }
    (body[..end].to_string(), true)
}

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

    #[test]
    fn reads_local_yoi_ticket_and_objective_records_without_migration() {
        let dir = tempfile::tempdir().unwrap();
        write_ticket(dir.path(), "00000000001J2", "Read bridge", "ready");
        write_objective(dir.path(), "00000000001J3", "Control plane", "active");

        let reader = LocalProjectRecordReader::new(dir.path());
        let tickets = reader.list_tickets(20).unwrap();
        assert_eq!(tickets.record_authority, "local_yoi_project_records");
        assert_eq!(tickets.items[0].id, "00000000001J2");
        assert_eq!(tickets.items[0].state, "ready");

        let ticket = reader.ticket("00000000001J2").unwrap();
        assert!(ticket.body.contains("Ticket body"));

        let objectives = reader.list_objectives(20).unwrap();
        assert_eq!(objectives.items[0].id, "00000000001J3");
        assert_eq!(objectives.items[0].linked_tickets, vec!["00000000001J2"]);

        let objective = reader.objective("00000000001J3").unwrap();
        assert!(objective.body.contains("Objective body"));
    }

    fn write_ticket(root: &Path, id: &str, title: &str, state: &str) {
        let ticket_dir = root.join(".yoi/tickets").join(id);
        fs::create_dir_all(&ticket_dir).unwrap();
        fs::write(
            ticket_dir.join("item.md"),
            format!(
                r#"---
title: "{title}"
state: "{state}"
created_at: "2026-01-01T00:00:00Z"
updated_at: "2026-01-02T00:00:00Z"
---

Ticket body.
"#,
            ),
        )
        .unwrap();
        fs::write(ticket_dir.join("thread.md"), "").unwrap();
    }

    fn write_objective(root: &Path, id: &str, title: &str, state: &str) {
        let objective_dir = root.join(".yoi/objectives").join(id);
        fs::create_dir_all(&objective_dir).unwrap();
        fs::write(
            objective_dir.join("item.md"),
            format!(
                r#"---
title: "{title}"
state: "{state}"
created_at: "2026-01-01T00:00:00Z"
updated_at: "2026-01-02T00:00:00Z"
linked_tickets: ["00000000001J2"]
---

Objective body.
"#,
            ),
        )
        .unwrap();
    }
}
