use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH};

use regex::Regex;
use scraper::{ElementRef, Html, Selector};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use thiserror::Error;
use uuid::Uuid;

use crate::pipe::{Action, AgentMessage, BrowserPipeTool, CommandOutput, Transport};

use super::zhihu_hotlist_store::{
    load_latest_snapshot, load_snapshot, persist_snapshot, resolve_store_dir,
    ZhihuCommentMetricSnapshot, ZhihuHotItemSnapshot, ZhihuHotlistCollectionStats,
    ZhihuHotlistSnapshot, ZhihuHotlistStoreError,
};

const COLLECTOR_VERSION: &str = "zhihu_hotlist_v1";
const DEFAULT_WAIT_TIMEOUT_MS: u64 = 5_000;
const DEFAULT_COMMENT_SCROLL_Y: i64 = 1_200;

fn default_top_n() -> usize {
    10
}

fn default_comments_per_item() -> usize {
    20
}

fn default_report_top_n() -> usize {
    10
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ZhihuHotlistCollectRequest {
    #[serde(default = "default_top_n")]
    pub top_n: usize,
    #[serde(default = "default_comments_per_item")]
    pub comments_per_item: usize,
    #[serde(default)]
    pub store_dir: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ZhihuHotlistCollectResult {
    pub summary: String,
    pub snapshot_id: String,
    pub item_count: usize,
    pub snapshot_path: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ZhihuHotlistReportRequest {
    #[serde(default)]
    pub snapshot_id: Option<String>,
    #[serde(default)]
    pub store_dir: Option<String>,
    #[serde(default = "default_report_top_n")]
    pub top_n: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ZhihuHotlistReportResult {
    pub summary: String,
    pub snapshot_id: String,
    pub item_count: usize,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ZhihuHotlistFlow {
    pub hotlist_url: String,
    pub domains: HashMap<String, String>,
    pub literals: HashMap<String, String>,
    pub selectors: HashMap<String, String>,
}

#[derive(Debug, Error)]
pub enum ZhihuHotlistSkillError {
    #[error("top_n must be greater than 0")]
    InvalidTopN,
    #[error("comments_per_item must be greater than 0")]
    InvalidCommentsPerItem,
    #[error("failed to load zhihu hotlist flow: {0}")]
    FlowLoad(String),
    #[error("missing selector in zhihu hotlist flow: {0}")]
    MissingSelector(String),
    #[error("missing domain in zhihu hotlist flow: {0}")]
    MissingDomain(String),
    #[error("missing literal in zhihu hotlist flow: {0}")]
    MissingLiteral(String),
    #[error("invalid selector in zhihu hotlist flow `{name}`: {message}")]
    InvalidSelector { name: String, message: String },
    #[error("browser action failed at step {step}: {message}")]
    BrowserActionFailed { step: String, message: String },
    #[error("zhihu hotlist page did not expose any items")]
    NoHotlistItems,
    #[error("zhihu hotlist html did not include enough data for item extraction")]
    IncompleteHotlistHtml,
    #[error(transparent)]
    Store(#[from] ZhihuHotlistStoreError),
}

pub fn default_flow_path() -> PathBuf {
    super::default_skill_resource_path("zhihu_hotlist_flow.json")
}

pub fn load_flow() -> Result<ZhihuHotlistFlow, ZhihuHotlistSkillError> {
    let path = default_flow_path();
    let contents = fs::read_to_string(&path)
        .map_err(|err| ZhihuHotlistSkillError::FlowLoad(format!("{} ({})", err, path.display())))?;
    serde_json::from_str(&contents)
        .map_err(|err| ZhihuHotlistSkillError::FlowLoad(format!("{} ({})", err, path.display())))
}

pub fn execute_collect<T: Transport>(
    transport: &T,
    browser_tool: &BrowserPipeTool<T>,
    req: ZhihuHotlistCollectRequest,
) -> Result<ZhihuHotlistCollectResult, ZhihuHotlistSkillError> {
    validate_collect_request(&req)?;
    let flow = load_flow()?;
    let zhihu_domain = resolve_domain(&flow, "zhihu")?;
    let hotlist_guard = resolve_literal(&flow, "hotlist_guard")?;
    let hotlist_root_selector = resolve_selector(&flow, "hotlist_root")?;
    let hotlist_item_selector = resolve_selector(&flow, "hotlist_item")?;
    let comment_list_selector = resolve_selector(&flow, "comment_list")?;
    let comment_item_selector = resolve_selector(&flow, "comment_item")?;
    let comment_metric_selector = resolve_selector(&flow, "comment_metric")?;

    let page_url = ensure_hotlist_page(
        transport,
        browser_tool,
        &flow,
        &zhihu_domain,
        hotlist_guard,
        hotlist_root_selector,
        hotlist_item_selector,
    )?;

    let hotlist_html = run_action(
        transport,
        browser_tool,
        "capture hotlist html",
        Action::GetHtml,
        json!({ "selector": hotlist_root_selector, "outer": true }),
        &zhihu_domain,
    )?;
    let hotlist_items = parse_hotlist_items(&hotlist_html.data, &flow, req.top_n)?;
    if hotlist_items.is_empty() {
        return Err(ZhihuHotlistSkillError::NoHotlistItems);
    }

    let mut items = Vec::with_capacity(hotlist_items.len());
    let mut partial_items = 0usize;
    let mut items_with_comment_metrics = 0usize;
    let mut total_comment_metric_records = 0usize;

    let comment_context = CommentCollectionContext {
        zhihu_domain: &zhihu_domain,
        comment_list_selector,
        comment_item_selector,
        comment_metric_selector,
        page_root_selector: hotlist_root_selector,
        comments_per_item: req.comments_per_item,
    };

    for hot_item in hotlist_items {
        let comment_metrics = match collect_comment_metrics(
            transport,
            browser_tool,
            &comment_context,
            &hot_item.url,
            hot_item.rank,
        ) {
            Ok(metrics) => metrics,
            Err(_) => {
                partial_items += 1;
                Vec::new()
            }
        };

        if !comment_metrics.is_empty() {
            items_with_comment_metrics += 1;
            total_comment_metric_records += comment_metrics.len();
        }

        items.push(ZhihuHotItemSnapshot {
            rank: hot_item.rank,
            item_id: hot_item.item_id,
            url: hot_item.url,
            title: hot_item.title,
            summary: hot_item.summary,
            heat_text: hot_item.heat_text.clone(),
            heat_value: parse_count_text(&hot_item.heat_text),
            comment_metrics,
        });
    }

    let snapshot = ZhihuHotlistSnapshot {
        snapshot_id: build_snapshot_id(),
        captured_at_ms: now_unix_ms(),
        page_url,
        collector_version: COLLECTOR_VERSION.to_string(),
        collection_stats: ZhihuHotlistCollectionStats {
            requested_items: req.top_n,
            collected_items: items.len(),
            items_with_comment_metrics,
            total_comment_metric_records,
            partial_items,
        },
        items,
    };

    let store_dir = resolve_store_dir(req.store_dir.as_deref());
    let persisted = persist_snapshot(&store_dir, &snapshot)?;
    let summary = format!(
        "知乎热榜快照已保存：{} 条热榜，{} 条评论指标记录 ({})",
        snapshot.items.len(),
        snapshot.collection_stats.total_comment_metric_records,
        persisted.snapshot_path.display()
    );

    Ok(ZhihuHotlistCollectResult {
        summary,
        snapshot_id: snapshot.snapshot_id,
        item_count: snapshot.items.len(),
        snapshot_path: persisted.snapshot_path.display().to_string(),
    })
}

pub fn execute_report(
    req: ZhihuHotlistReportRequest,
) -> Result<ZhihuHotlistReportResult, ZhihuHotlistSkillError> {
    validate_report_request(&req)?;
    let store_dir = resolve_store_dir(req.store_dir.as_deref());
    let snapshot = match req.snapshot_id.as_deref() {
        Some(snapshot_id) if !snapshot_id.trim().is_empty() => {
            load_snapshot(&store_dir, snapshot_id.trim())?
        }
        _ => load_latest_snapshot(&store_dir)?,
    };

    let mut lines = vec![format!(
        "知乎热榜报告 {}: 共 {} 条，采集于 {}",
        snapshot.snapshot_id,
        snapshot.items.len(),
        snapshot.captured_at_ms
    )];

    for item in snapshot.items.iter().take(req.top_n) {
        let totals = aggregate_comment_metrics(&item.comment_metrics);
        lines.push(format!(
            "{}. {} | 热度 {} | 评论指标 {} 条 | 回复 {} | 赞同 {} | 收藏 {} | 红心 {}",
            item.rank,
            item.title,
            item.heat_text,
            item.comment_metrics.len(),
            totals.reply_count,
            totals.upvote_count,
            totals.favorite_count,
            totals.heart_count,
        ));
    }

    Ok(ZhihuHotlistReportResult {
        summary: lines.join("\n"),
        snapshot_id: snapshot.snapshot_id,
        item_count: snapshot.items.len(),
    })
}

fn validate_collect_request(
    req: &ZhihuHotlistCollectRequest,
) -> Result<(), ZhihuHotlistSkillError> {
    if req.top_n == 0 {
        return Err(ZhihuHotlistSkillError::InvalidTopN);
    }
    if req.comments_per_item == 0 {
        return Err(ZhihuHotlistSkillError::InvalidCommentsPerItem);
    }
    Ok(())
}

fn validate_report_request(req: &ZhihuHotlistReportRequest) -> Result<(), ZhihuHotlistSkillError> {
    if req.top_n == 0 {
        return Err(ZhihuHotlistSkillError::InvalidTopN);
    }
    Ok(())
}

fn ensure_hotlist_page<T: Transport>(
    transport: &T,
    browser_tool: &BrowserPipeTool<T>,
    flow: &ZhihuHotlistFlow,
    zhihu_domain: &str,
    hotlist_guard: &str,
    hotlist_root_selector: &str,
    hotlist_item_selector: &str,
) -> Result<String, ZhihuHotlistSkillError> {
    let hotlist_probe = run_action(
        transport,
        browser_tool,
        "probe current Zhihu page for hotlist guard",
        Action::GetText,
        json!({ "selector": hotlist_root_selector }),
        zhihu_domain,
    );

    if let Ok(result) = hotlist_probe {
        let text = extract_text(&result.data);
        if text.contains(hotlist_guard) {
            run_action(
                transport,
                browser_tool,
                "wait for hotlist items on current page",
                Action::WaitForSelector,
                json!({ "selector": hotlist_item_selector, "timeout_ms": DEFAULT_WAIT_TIMEOUT_MS }),
                zhihu_domain,
            )?;
            return Ok(extract_url(&result.data).unwrap_or_else(|| flow.hotlist_url.clone()));
        }
    }

    let navigate = run_action(
        transport,
        browser_tool,
        "navigate to Zhihu hotlist",
        Action::Navigate,
        json!({ "url": flow.hotlist_url }),
        zhihu_domain,
    )?;
    run_action(
        transport,
        browser_tool,
        "wait for Zhihu hotlist items",
        Action::WaitForSelector,
        json!({ "selector": hotlist_item_selector, "timeout_ms": DEFAULT_WAIT_TIMEOUT_MS }),
        zhihu_domain,
    )?;

    Ok(extract_url(&navigate.data).unwrap_or_else(|| flow.hotlist_url.clone()))
}

fn collect_comment_metrics<T: Transport>(
    transport: &T,
    browser_tool: &BrowserPipeTool<T>,
    context: &CommentCollectionContext<'_>,
    item_url: &str,
    rank: usize,
) -> Result<Vec<ZhihuCommentMetricSnapshot>, ZhihuHotlistSkillError> {
    let step_prefix = format!("collect comment metrics for hot item #{rank}");
    run_action(
        transport,
        browser_tool,
        &format!("{step_prefix}: navigate detail page"),
        Action::Navigate,
        json!({ "url": item_url }),
        context.zhihu_domain,
    )?;
    run_action(
        transport,
        browser_tool,
        &format!("{step_prefix}: wait for page root"),
        Action::WaitForSelector,
        json!({ "selector": context.page_root_selector, "timeout_ms": DEFAULT_WAIT_TIMEOUT_MS }),
        context.zhihu_domain,
    )?;
    run_action(
        transport,
        browser_tool,
        &format!("{step_prefix}: scroll toward comments"),
        Action::ScrollTo,
        json!({ "y": DEFAULT_COMMENT_SCROLL_Y }),
        context.zhihu_domain,
    )?;
    run_action(
        transport,
        browser_tool,
        &format!("{step_prefix}: wait for comment list"),
        Action::WaitForSelector,
        json!({ "selector": context.comment_list_selector, "timeout_ms": DEFAULT_WAIT_TIMEOUT_MS }),
        context.zhihu_domain,
    )?;
    run_action(
        transport,
        browser_tool,
        &format!("{step_prefix}: scroll comment list into view"),
        Action::ScrollTo,
        json!({ "selector": context.comment_list_selector }),
        context.zhihu_domain,
    )?;
    let comments_html = run_action(
        transport,
        browser_tool,
        &format!("{step_prefix}: capture page html for comments"),
        Action::GetHtml,
        json!({ "selector": context.page_root_selector, "outer": true }),
        context.zhihu_domain,
    )?;

    Ok(parse_comment_metrics(
        &comments_html.data,
        context.comment_list_selector,
        context.comment_item_selector,
        context.comment_metric_selector,
        context.comments_per_item,
    ))
}

fn run_action<T: Transport>(
    transport: &T,
    browser_tool: &BrowserPipeTool<T>,
    step: &str,
    action: Action,
    params: Value,
    expected_domain: &str,
) -> Result<CommandOutput, ZhihuHotlistSkillError> {
    transport
        .send(&AgentMessage::LogEntry {
            level: "info".to_string(),
            message: step.to_string(),
        })
        .map_err(|err| ZhihuHotlistSkillError::BrowserActionFailed {
            step: step.to_string(),
            message: err.to_string(),
        })?;

    let result = browser_tool
        .invoke(action, params, expected_domain)
        .map_err(|err| ZhihuHotlistSkillError::BrowserActionFailed {
            step: step.to_string(),
            message: err.to_string(),
        })?;

    if !result.success {
        return Err(ZhihuHotlistSkillError::BrowserActionFailed {
            step: step.to_string(),
            message: result.data.to_string(),
        });
    }

    Ok(result)
}

fn parse_hotlist_items(
    data: &Value,
    flow: &ZhihuHotlistFlow,
    top_n: usize,
) -> Result<Vec<ParsedHotItem>, ZhihuHotlistSkillError> {
    let html = extract_html(data);
    if html.trim().is_empty() {
        return Err(ZhihuHotlistSkillError::IncompleteHotlistHtml);
    }

    let document = Html::parse_document(&html);
    let item_selector = parse_selector("hotlist_item", resolve_selector(flow, "hotlist_item")?)?;
    let title_link_selector = parse_selector(
        "hotlist_title_link",
        resolve_selector(flow, "hotlist_title_link")?,
    )?;
    let summary_selector = parse_selector(
        "hotlist_summary",
        resolve_selector(flow, "hotlist_summary")?,
    )?;
    let heat_selector = parse_selector("hotlist_heat", resolve_selector(flow, "hotlist_heat")?)?;

    let mut seen_urls = HashSet::new();
    let mut items = Vec::new();

    for (index, element) in document.select(&item_selector).enumerate() {
        let Some(link) = element.select(&title_link_selector).next() else {
            continue;
        };

        let title = compact_text(&link);
        if title.is_empty() {
            continue;
        }

        let href = link.value().attr("href").unwrap_or_default();
        let url = normalize_zhihu_url(href);
        if url.is_empty() || !seen_urls.insert(url.clone()) {
            continue;
        }

        let summary = element
            .select(&summary_selector)
            .next()
            .map(|node| compact_text(&node))
            .unwrap_or_default();
        let heat_text = element
            .select(&heat_selector)
            .next()
            .map(|node| compact_text(&node))
            .unwrap_or_default();

        items.push(ParsedHotItem {
            rank: index + 1,
            item_id: derive_item_id(&url),
            url,
            title,
            summary,
            heat_text,
        });
        if items.len() >= top_n {
            break;
        }
    }

    if items.is_empty() {
        return Err(ZhihuHotlistSkillError::NoHotlistItems);
    }

    for (index, item) in items.iter_mut().enumerate() {
        item.rank = index + 1;
    }

    Ok(items)
}

fn parse_comment_metrics(
    data: &Value,
    comment_list_selector: &str,
    comment_item_selector: &str,
    comment_metric_selector: &str,
    comments_per_item: usize,
) -> Vec<ZhihuCommentMetricSnapshot> {
    let html = extract_html(data);
    if html.trim().is_empty() {
        return Vec::new();
    }

    let document = Html::parse_document(&html);
    let comment_item_selector = match Selector::parse(comment_item_selector) {
        Ok(selector) => selector,
        Err(_) => return Vec::new(),
    };
    let metric_selector = match Selector::parse(comment_metric_selector) {
        Ok(selector) => selector,
        Err(_) => return Vec::new(),
    };
    let comment_list_selector = match Selector::parse(comment_list_selector) {
        Ok(selector) => selector,
        Err(_) => return Vec::new(),
    };

    let container = document
        .select(&comment_list_selector)
        .next()
        .map(|node| node.html())
        .unwrap_or_else(|| html.clone());
    let scoped_document = Html::parse_fragment(&container);

    scoped_document
        .select(&comment_item_selector)
        .take(comments_per_item)
        .enumerate()
        .map(|(index, element)| {
            build_comment_metric_snapshot(index + 1, &element, &metric_selector)
        })
        .collect()
}

fn build_comment_metric_snapshot(
    position: usize,
    element: &ElementRef<'_>,
    metric_selector: &Selector,
) -> ZhihuCommentMetricSnapshot {
    let mut raw_metrics = BTreeMap::new();
    let mut snapshot = ZhihuCommentMetricSnapshot {
        position,
        comment_id: element
            .value()
            .attr("data-id")
            .or_else(|| element.value().attr("data-comment-id"))
            .or_else(|| element.value().attr("id"))
            .map(ToString::to_string),
        reply_count: None,
        upvote_count: None,
        favorite_count: None,
        heart_count: None,
        raw_metrics: None,
    };

    for metric in element.select(metric_selector) {
        let text = compact_text(&metric);
        if text.is_empty() {
            continue;
        }

        let count = parse_count_text(&text).or(Some(0));
        let lowered = text.to_ascii_lowercase();
        if text.contains("回复") {
            snapshot.reply_count = count;
        } else if text.contains("赞") || lowered.contains("upvote") {
            snapshot.upvote_count = count;
        } else if text.contains("收藏")
            || lowered.contains("favorite")
            || lowered.contains("bookmark")
        {
            snapshot.favorite_count = count;
        } else if text.contains("喜欢")
            || text.contains("红心")
            || text.contains('❤')
            || text.contains('♥')
        {
            snapshot.heart_count = count;
        } else if let Some(value) = count {
            raw_metrics.insert(sanitize_metric_key(&text), value);
        }
    }

    if !raw_metrics.is_empty() {
        snapshot.raw_metrics = Some(raw_metrics);
    }

    snapshot
}

fn parse_selector(name: &str, raw: &str) -> Result<Selector, ZhihuHotlistSkillError> {
    Selector::parse(raw).map_err(|err| ZhihuHotlistSkillError::InvalidSelector {
        name: name.to_string(),
        message: err.to_string(),
    })
}

fn resolve_selector<'a>(
    flow: &'a ZhihuHotlistFlow,
    key: &str,
) -> Result<&'a str, ZhihuHotlistSkillError> {
    flow.selectors
        .get(key)
        .map(String::as_str)
        .ok_or_else(|| ZhihuHotlistSkillError::MissingSelector(key.to_string()))
}

fn resolve_domain(flow: &ZhihuHotlistFlow, key: &str) -> Result<String, ZhihuHotlistSkillError> {
    flow.domains
        .get(key)
        .cloned()
        .ok_or_else(|| ZhihuHotlistSkillError::MissingDomain(key.to_string()))
}

fn resolve_literal<'a>(
    flow: &'a ZhihuHotlistFlow,
    key: &str,
) -> Result<&'a str, ZhihuHotlistSkillError> {
    flow.literals
        .get(key)
        .map(String::as_str)
        .ok_or_else(|| ZhihuHotlistSkillError::MissingLiteral(key.to_string()))
}

fn extract_text(data: &Value) -> String {
    data.get("text")
        .and_then(Value::as_str)
        .or_else(|| data.as_str())
        .unwrap_or_default()
        .trim()
        .to_string()
}

fn extract_html(data: &Value) -> String {
    data.get("html")
        .and_then(Value::as_str)
        .or_else(|| data.get("outer_html").and_then(Value::as_str))
        .or_else(|| data.as_str())
        .unwrap_or_default()
        .to_string()
}

fn extract_url(data: &Value) -> Option<String> {
    data.get("url")
        .and_then(Value::as_str)
        .map(ToString::to_string)
}

fn compact_text(element: &ElementRef<'_>) -> String {
    element
        .text()
        .map(str::trim)
        .filter(|text| !text.is_empty())
        .collect::<Vec<_>>()
        .join(" ")
}

fn normalize_zhihu_url(raw: &str) -> String {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return String::new();
    }
    if trimmed.starts_with("https://") || trimmed.starts_with("http://") {
        return trimmed.split('#').next().unwrap_or(trimmed).to_string();
    }
    if let Some(rest) = trimmed.strip_prefix("//") {
        return format!("https://{}", rest.split('#').next().unwrap_or(rest));
    }
    if trimmed.starts_with('/') {
        return format!("https://www.zhihu.com{}", trimmed);
    }
    format!("https://www.zhihu.com/{}", trimmed.trim_start_matches('/'))
}

fn derive_item_id(url: &str) -> String {
    let normalized = url
        .trim()
        .trim_start_matches("https://")
        .trim_start_matches("http://");
    let path = normalized
        .split_once('/')
        .map(|(_, path)| path)
        .unwrap_or_default()
        .split('?')
        .next()
        .unwrap_or_default()
        .trim_matches('/');
    if path.is_empty() {
        "root".to_string()
    } else {
        path.replace('/', "_")
    }
}

fn aggregate_comment_metrics(metrics: &[ZhihuCommentMetricSnapshot]) -> AggregatedCommentMetrics {
    let mut totals = AggregatedCommentMetrics::default();
    for metric in metrics {
        totals.reply_count += metric.reply_count.unwrap_or(0);
        totals.upvote_count += metric.upvote_count.unwrap_or(0);
        totals.favorite_count += metric.favorite_count.unwrap_or(0);
        totals.heart_count += metric.heart_count.unwrap_or(0);
    }
    totals
}

fn sanitize_metric_key(text: &str) -> String {
    let compact = text
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() {
                ch.to_ascii_lowercase()
            } else {
                '_'
            }
        })
        .collect::<String>();
    compact.trim_matches('_').to_string()
}

fn build_snapshot_id() -> String {
    format!("{}-{}", now_unix_ms(), Uuid::new_v4())
}

fn now_unix_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64
}

fn parse_count_text(text: &str) -> Option<u64> {
    let compact = text.replace([',', ' '], "");
    let captures = count_regex().captures(&compact)?;
    let number = captures.name("number")?.as_str().parse::<f64>().ok()?;
    let unit = captures
        .name("unit")
        .map(|unit| unit.as_str())
        .unwrap_or_default();
    let multiplier = match unit {
        "万" | "w" | "W" => 10_000f64,
        "亿" => 100_000_000f64,
        "k" | "K" => 1_000f64,
        "m" | "M" => 1_000_000f64,
        _ => 1f64,
    };
    Some((number * multiplier).round() as u64)
}

fn count_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r"(?P<number>\d+(?:\.\d+)?)\s*(?P<unit>万|亿|[kKmMwW])?").unwrap()
    })
}

#[derive(Debug, Clone, Copy)]
struct CommentCollectionContext<'a> {
    zhihu_domain: &'a str,
    comment_list_selector: &'a str,
    comment_item_selector: &'a str,
    comment_metric_selector: &'a str,
    page_root_selector: &'a str,
    comments_per_item: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ParsedHotItem {
    rank: usize,
    item_id: String,
    url: String,
    title: String,
    summary: String,
    heat_text: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct AggregatedCommentMetrics {
    reply_count: u64,
    upvote_count: u64,
    favorite_count: u64,
    heart_count: u64,
}
