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

use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ZhihuHotlistIndex {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub latest_snapshot_id: Option<String>,
    #[serde(default)]
    pub snapshots: Vec<ZhihuHotlistIndexEntry>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZhihuHotlistIndexEntry {
    pub snapshot_id: String,
    pub captured_at_ms: u64,
    pub path: String,
    pub item_count: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZhihuHotlistSnapshot {
    pub snapshot_id: String,
    pub captured_at_ms: u64,
    pub page_url: String,
    pub collector_version: String,
    pub items: Vec<ZhihuHotItemSnapshot>,
    pub collection_stats: ZhihuHotlistCollectionStats,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZhihuHotItemSnapshot {
    pub rank: usize,
    pub item_id: String,
    pub url: String,
    pub title: String,
    pub summary: String,
    pub heat_text: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub heat_value: Option<u64>,
    #[serde(default)]
    pub comment_metrics: Vec<ZhihuCommentMetricSnapshot>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZhihuCommentMetricSnapshot {
    pub position: usize,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub comment_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reply_count: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub upvote_count: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub favorite_count: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub heart_count: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub raw_metrics: Option<BTreeMap<String, u64>>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZhihuHotlistCollectionStats {
    pub requested_items: usize,
    pub collected_items: usize,
    pub items_with_comment_metrics: usize,
    pub total_comment_metric_records: usize,
    pub partial_items: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PersistedSnapshotPaths {
    pub snapshot_path: PathBuf,
    pub index_path: PathBuf,
}

#[derive(Debug, Error)]
pub enum ZhihuHotlistStoreError {
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("json error: {0}")]
    Json(#[from] serde_json::Error),
    #[error("no persisted Zhihu hotlist snapshots found")]
    NoSnapshots,
    #[error("snapshot not found: {0}")]
    SnapshotNotFound(String),
}

pub fn default_store_dir() -> PathBuf {
    std::env::current_dir()
        .unwrap_or_else(|_| PathBuf::from("."))
        .join("data")
        .join("zhihu_hotlist")
}

pub fn resolve_store_dir(store_dir: Option<&str>) -> PathBuf {
    match store_dir {
        Some(path) if !path.trim().is_empty() => PathBuf::from(path),
        _ => default_store_dir(),
    }
}

pub fn persist_snapshot(
    base_dir: &Path,
    snapshot: &ZhihuHotlistSnapshot,
) -> Result<PersistedSnapshotPaths, ZhihuHotlistStoreError> {
    let snapshot_dir = snapshots_dir(base_dir);
    fs::create_dir_all(&snapshot_dir)?;

    let snapshot_rel_path = format!("snapshots/{}.json", snapshot.snapshot_id);
    let snapshot_path = base_dir.join(&snapshot_rel_path);
    fs::write(&snapshot_path, serde_json::to_vec_pretty(snapshot)?)?;

    let mut index = load_index(base_dir)?;
    index.latest_snapshot_id = Some(snapshot.snapshot_id.clone());
    index
        .snapshots
        .retain(|entry| entry.snapshot_id != snapshot.snapshot_id);
    index.snapshots.push(ZhihuHotlistIndexEntry {
        snapshot_id: snapshot.snapshot_id.clone(),
        captured_at_ms: snapshot.captured_at_ms,
        path: snapshot_rel_path,
        item_count: snapshot.items.len(),
    });
    index
        .snapshots
        .sort_by(|left, right| left.captured_at_ms.cmp(&right.captured_at_ms));

    let index_path = index_path(base_dir);
    fs::write(&index_path, serde_json::to_vec_pretty(&index)?)?;

    Ok(PersistedSnapshotPaths {
        snapshot_path,
        index_path,
    })
}

pub fn load_index(base_dir: &Path) -> Result<ZhihuHotlistIndex, ZhihuHotlistStoreError> {
    let path = index_path(base_dir);
    if !path.exists() {
        return Ok(ZhihuHotlistIndex::default());
    }

    let contents = fs::read_to_string(path)?;
    Ok(serde_json::from_str(&contents)?)
}

pub fn load_snapshot(
    base_dir: &Path,
    snapshot_id: &str,
) -> Result<ZhihuHotlistSnapshot, ZhihuHotlistStoreError> {
    let path = base_dir
        .join("snapshots")
        .join(format!("{}.json", snapshot_id.trim()));
    if !path.exists() {
        return Err(ZhihuHotlistStoreError::SnapshotNotFound(
            snapshot_id.trim().to_string(),
        ));
    }

    let contents = fs::read_to_string(path)?;
    Ok(serde_json::from_str(&contents)?)
}

pub fn load_latest_snapshot(
    base_dir: &Path,
) -> Result<ZhihuHotlistSnapshot, ZhihuHotlistStoreError> {
    let index = load_index(base_dir)?;
    let snapshot_id = index
        .latest_snapshot_id
        .ok_or(ZhihuHotlistStoreError::NoSnapshots)?;
    load_snapshot(base_dir, &snapshot_id)
}

fn index_path(base_dir: &Path) -> PathBuf {
    base_dir.join("index.json")
}

fn snapshots_dir(base_dir: &Path) -> PathBuf {
    base_dir.join("snapshots")
}
