use crate::cli::render::{self, Icons, Theme};
use serde::Serialize;

use crate::Config;
use crate::cli::progress::stage_spinner_v2;
use crate::core::db::create_connection;
use crate::core::error::{LoreError, Result};
use crate::core::paths::get_db_path;
use crate::core::project::resolve_project;
use crate::core::time::{ms_to_iso, parse_since};
use crate::embedding::ollama::{OllamaClient, OllamaConfig};
use crate::timeline::collect::collect_events;
use crate::timeline::expand::expand_timeline;
use crate::timeline::seed::{seed_timeline, seed_timeline_direct};
use crate::timeline::{
    EntityRef, ExpandedEntityRef, TimelineEvent, TimelineEventType, TimelineResult, UnresolvedRef,
};

/// Parameters for running the timeline pipeline.
pub struct TimelineParams {
    pub query: String,
    pub project: Option<String>,
    pub since: Option<String>,
    pub depth: u32,
    pub no_mentions: bool,
    pub limit: usize,
    pub max_seeds: usize,
    pub max_entities: usize,
    pub max_evidence: usize,
    pub robot_mode: bool,
}

/// Parsed timeline query: either a search string or a direct entity reference.
enum TimelineQuery {
    Search(String),
    EntityDirect { entity_type: String, iid: i64 },
}

/// Parse the timeline query for entity-direct patterns.
///
/// Recognized patterns (case-insensitive prefix):
/// - `issue:N`, `i:N` -> issue
/// - `mr:N`, `m:N` -> merge_request
/// - Anything else -> search query
fn parse_timeline_query(query: &str) -> TimelineQuery {
    let query = query.trim();
    if let Some((prefix, rest)) = query.split_once(':') {
        let prefix_lower = prefix.to_ascii_lowercase();
        if let Ok(iid) = rest.trim().parse::<i64>() {
            match prefix_lower.as_str() {
                "issue" | "i" => {
                    return TimelineQuery::EntityDirect {
                        entity_type: "issue".to_owned(),
                        iid,
                    };
                }
                "mr" | "m" => {
                    return TimelineQuery::EntityDirect {
                        entity_type: "merge_request".to_owned(),
                        iid,
                    };
                }
                _ => {}
            }
        }
    }
    TimelineQuery::Search(query.to_owned())
}

/// Run the full timeline pipeline: SEED -> EXPAND -> COLLECT.
pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<TimelineResult> {
    let db_path = get_db_path(config.storage.db_path.as_deref());
    let conn = create_connection(&db_path)?;

    let project_id = params
        .project
        .as_deref()
        .map(|p| resolve_project(&conn, p))
        .transpose()?;

    let since_ms = params
        .since
        .as_deref()
        .map(|s| {
            parse_since(s).ok_or_else(|| {
                LoreError::Other(format!(
                    "Invalid --since value: '{s}'. Use a duration (7d, 2w, 6m) or date (2024-01-15)"
                ))
            })
        })
        .transpose()?;

    // Parse query for entity-direct syntax (issue:N, mr:N, i:N, m:N)
    let parsed_query = parse_timeline_query(&params.query);

    let seed_result = match parsed_query {
        TimelineQuery::EntityDirect { entity_type, iid } => {
            // Direct seeding: synchronous, no Ollama needed
            let spinner = stage_spinner_v2(
                Icons::search(),
                "Resolve",
                "Resolving entity...",
                params.robot_mode,
            );
            let result = seed_timeline_direct(&conn, &entity_type, iid, project_id)?;
            spinner.finish_and_clear();
            result
        }
        TimelineQuery::Search(ref query) => {
            // Construct OllamaClient for hybrid search (same pattern as run_search)
            let ollama_cfg = &config.embedding;
            let client = OllamaClient::new(OllamaConfig {
                base_url: ollama_cfg.base_url.clone(),
                model: ollama_cfg.model.clone(),
                ..OllamaConfig::default()
            });

            // Stage 1+2: SEED + HYDRATE (hybrid search with FTS fallback)
            let spinner = stage_spinner_v2(
                Icons::search(),
                "Seed",
                "Seeding timeline...",
                params.robot_mode,
            );
            let result = seed_timeline(
                &conn,
                Some(&client),
                query,
                project_id,
                since_ms,
                params.max_seeds,
                params.max_evidence,
            )
            .await?;
            spinner.finish_and_clear();
            result
        }
    };

    // Stage 3: EXPAND
    let spinner = stage_spinner_v2(
        Icons::sync(),
        "Expand",
        "Expanding cross-references...",
        params.robot_mode,
    );
    let expand_result = expand_timeline(
        &conn,
        &seed_result.seed_entities,
        params.depth,
        !params.no_mentions,
        params.max_entities,
    )?;
    spinner.finish_and_clear();

    // Stage 4: COLLECT
    let spinner = stage_spinner_v2(
        Icons::sync(),
        "Collect",
        "Collecting events...",
        params.robot_mode,
    );
    let (events, total_before_limit) = collect_events(
        &conn,
        &seed_result.seed_entities,
        &expand_result.expanded_entities,
        &seed_result.evidence_notes,
        &seed_result.matched_discussions,
        since_ms,
        params.limit,
    )?;
    spinner.finish_and_clear();

    Ok(TimelineResult {
        query: params.query.clone(),
        search_mode: seed_result.search_mode,
        events,
        total_filtered_events: total_before_limit,
        seed_entities: seed_result.seed_entities,
        expanded_entities: expand_result.expanded_entities,
        unresolved_references: expand_result.unresolved_references,
    })
}

// ─── Human output ────────────────────────────────────────────────────────────

/// Render timeline as colored human-readable output.
pub fn print_timeline(result: &TimelineResult) {
    let entity_count = result.seed_entities.len() + result.expanded_entities.len();

    println!();
    println!(
        "{}",
        Theme::bold().render(&format!(
            "Timeline: \"{}\" ({} events across {} entities)",
            result.query,
            result.events.len(),
            entity_count,
        ))
    );
    println!("{}", "\u{2500}".repeat(60));
    println!();

    if result.events.is_empty() {
        println!(
            "  {}",
            Theme::dim().render("No events found for this query.")
        );
        println!();
        return;
    }

    for event in &result.events {
        print_timeline_event(event);
    }

    println!();
    println!("{}", "\u{2500}".repeat(60));
    print_timeline_footer(result);
}

fn print_timeline_event(event: &TimelineEvent) {
    let date = render::format_date(event.timestamp);
    let tag = format_event_tag(&event.event_type);
    let entity_icon = match event.entity_type.as_str() {
        "issue" => Icons::issue_opened(),
        "merge_request" => Icons::mr_opened(),
        _ => "",
    };
    let entity_ref = format_entity_ref(&event.entity_type, event.entity_iid);
    let actor = event
        .actor
        .as_deref()
        .map(|a| format!("@{a}"))
        .unwrap_or_default();
    let expanded_marker = if event.is_seed { "" } else { " [expanded]" };

    let summary_width = render::flex_width(40, 20);
    let summary = render::truncate(&event.summary, summary_width);
    println!("{date}  {tag}  {entity_icon}{entity_ref:7}  {summary}  {actor}{expanded_marker}");

    // Show snippet for evidence notes
    if let TimelineEventType::NoteEvidence { snippet, .. } = &event.event_type
        && !snippet.is_empty()
    {
        let mut lines = render::wrap_lines(snippet, 60);
        lines.truncate(4);
        for line in lines {
            println!(
                "                                  \"{}\"",
                Theme::dim().render(&line)
            );
        }
    }

    // Show full discussion thread
    if let TimelineEventType::DiscussionThread { notes, .. } = &event.event_type {
        let bar = "\u{2500}".repeat(44);
        println!("    \u{2500}\u{2500} Discussion {bar}");
        for note in notes {
            let note_date = render::format_date(note.created_at);
            let author = note
                .author
                .as_deref()
                .map(|a| format!("@{a}"))
                .unwrap_or_else(|| "unknown".to_owned());
            println!("    {} ({note_date}):", Theme::bold().render(&author));
            for line in render::wrap_lines(&note.body, 60) {
                println!("      {line}");
            }
        }
        println!("    {}", "\u{2500}".repeat(60));
    }
}

fn print_timeline_footer(result: &TimelineResult) {
    println!(
        "  Seed entities: {}",
        result
            .seed_entities
            .iter()
            .map(|e| format_entity_ref(&e.entity_type, e.entity_iid))
            .collect::<Vec<_>>()
            .join(", ")
    );

    if !result.expanded_entities.is_empty() {
        println!(
            "  Expanded:      {} entities via cross-references",
            result.expanded_entities.len()
        );
    }

    if !result.unresolved_references.is_empty() {
        println!(
            "  Unresolved:    {} external references",
            result.unresolved_references.len()
        );
    }

    println!();
}

/// Format event tag: pad plain text to TAG_WIDTH, then apply style.
const TAG_WIDTH: usize = 11;

fn format_event_tag(event_type: &TimelineEventType) -> String {
    let (label, style) = match event_type {
        TimelineEventType::Created => ("CREATED", Theme::success()),
        TimelineEventType::StateChanged { state } => match state.as_str() {
            "closed" => ("CLOSED", Theme::error()),
            "reopened" => ("REOPENED", Theme::warning()),
            _ => return style_padded(&state.to_uppercase(), TAG_WIDTH, Theme::dim()),
        },
        TimelineEventType::LabelAdded { .. } => ("LABEL+", Theme::info()),
        TimelineEventType::LabelRemoved { .. } => ("LABEL-", Theme::info()),
        TimelineEventType::MilestoneSet { .. } => ("MILESTONE+", Theme::accent()),
        TimelineEventType::MilestoneRemoved { .. } => ("MILESTONE-", Theme::accent()),
        TimelineEventType::Merged => ("MERGED", Theme::info()),
        TimelineEventType::NoteEvidence { .. } => ("NOTE", Theme::dim()),
        TimelineEventType::DiscussionThread { .. } => ("THREAD", Theme::warning()),
        TimelineEventType::CrossReferenced { .. } => ("REF", Theme::dim()),
    };
    style_padded(label, TAG_WIDTH, style)
}

/// Pad text to width, then apply lipgloss style (so ANSI codes don't break alignment).
fn style_padded(text: &str, width: usize, style: lipgloss::Style) -> String {
    let padded = format!("{:<width$}", text);
    style.render(&padded)
}

fn format_entity_ref(entity_type: &str, iid: i64) -> String {
    match entity_type {
        "issue" => format!("#{iid}"),
        "merge_request" => format!("!{iid}"),
        _ => format!("{entity_type}:{iid}"),
    }
}

// ─── Robot JSON output ───────────────────────────────────────────────────────

/// Render timeline as robot-mode JSON in {ok, data, meta} envelope.
pub fn print_timeline_json_with_meta(
    result: &TimelineResult,
    total_filtered_events: usize,
    depth: u32,
    include_mentions: bool,
    fields: Option<&[String]>,
) {
    let output = TimelineJsonEnvelope {
        ok: true,
        data: TimelineDataJson::from_result(result),
        meta: TimelineMetaJson {
            search_mode: result.search_mode.clone(),
            expansion_depth: depth,
            include_mentions,
            total_entities: result.seed_entities.len() + result.expanded_entities.len(),
            total_events: total_filtered_events,
            evidence_notes_included: count_evidence_notes(&result.events),
            discussion_threads_included: count_discussion_threads(&result.events),
            unresolved_references: result.unresolved_references.len(),
            showing: result.events.len(),
        },
    };

    let mut value = match serde_json::to_value(&output) {
        Ok(v) => v,
        Err(e) => {
            eprintln!("Error serializing timeline JSON: {e}");
            return;
        }
    };
    if let Some(f) = fields {
        let expanded = crate::cli::robot::expand_fields_preset(f, "timeline");
        crate::cli::robot::filter_fields(&mut value, "events", &expanded);
    }
    match serde_json::to_string(&value) {
        Ok(json) => println!("{json}"),
        Err(e) => eprintln!("Error serializing to JSON: {e}"),
    }
}

#[derive(Serialize)]
struct TimelineJsonEnvelope {
    ok: bool,
    data: TimelineDataJson,
    meta: TimelineMetaJson,
}

#[derive(Serialize)]
struct TimelineDataJson {
    query: String,
    event_count: usize,
    seed_entities: Vec<EntityJson>,
    expanded_entities: Vec<ExpandedEntityJson>,
    unresolved_references: Vec<UnresolvedRefJson>,
    events: Vec<EventJson>,
}

impl TimelineDataJson {
    fn from_result(result: &TimelineResult) -> Self {
        Self {
            query: result.query.clone(),
            event_count: result.events.len(),
            seed_entities: result.seed_entities.iter().map(EntityJson::from).collect(),
            expanded_entities: result
                .expanded_entities
                .iter()
                .map(ExpandedEntityJson::from)
                .collect(),
            unresolved_references: result
                .unresolved_references
                .iter()
                .map(UnresolvedRefJson::from)
                .collect(),
            events: result.events.iter().map(EventJson::from).collect(),
        }
    }
}

#[derive(Serialize)]
struct EntityJson {
    #[serde(rename = "type")]
    entity_type: String,
    iid: i64,
    project: String,
}

impl From<&EntityRef> for EntityJson {
    fn from(e: &EntityRef) -> Self {
        Self {
            entity_type: e.entity_type.clone(),
            iid: e.entity_iid,
            project: e.project_path.clone(),
        }
    }
}

#[derive(Serialize)]
struct ExpandedEntityJson {
    #[serde(rename = "type")]
    entity_type: String,
    iid: i64,
    project: String,
    depth: u32,
    via: ViaJson,
}

impl From<&ExpandedEntityRef> for ExpandedEntityJson {
    fn from(e: &ExpandedEntityRef) -> Self {
        Self {
            entity_type: e.entity_ref.entity_type.clone(),
            iid: e.entity_ref.entity_iid,
            project: e.entity_ref.project_path.clone(),
            depth: e.depth,
            via: ViaJson {
                from: EntityJson::from(&e.via_from),
                reference_type: e.via_reference_type.clone(),
                source_method: e.via_source_method.clone(),
            },
        }
    }
}

#[derive(Serialize)]
struct ViaJson {
    from: EntityJson,
    reference_type: String,
    source_method: String,
}

#[derive(Serialize)]
struct UnresolvedRefJson {
    source: EntityJson,
    target_project: Option<String>,
    target_type: String,
    target_iid: Option<i64>,
    reference_type: String,
}

impl From<&UnresolvedRef> for UnresolvedRefJson {
    fn from(r: &UnresolvedRef) -> Self {
        Self {
            source: EntityJson::from(&r.source),
            target_project: r.target_project.clone(),
            target_type: r.target_type.clone(),
            target_iid: r.target_iid,
            reference_type: r.reference_type.clone(),
        }
    }
}

#[derive(Serialize)]
struct EventJson {
    timestamp: String,
    entity_type: String,
    entity_iid: i64,
    project: String,
    event_type: String,
    summary: String,
    actor: Option<String>,
    url: Option<String>,
    is_seed: bool,
    details: serde_json::Value,
}

impl From<&TimelineEvent> for EventJson {
    fn from(e: &TimelineEvent) -> Self {
        let (event_type, details) = event_type_to_json(&e.event_type);
        Self {
            timestamp: ms_to_iso(e.timestamp),
            entity_type: e.entity_type.clone(),
            entity_iid: e.entity_iid,
            project: e.project_path.clone(),
            event_type,
            summary: e.summary.clone(),
            actor: e.actor.clone(),
            url: e.url.clone(),
            is_seed: e.is_seed,
            details,
        }
    }
}

fn event_type_to_json(event_type: &TimelineEventType) -> (String, serde_json::Value) {
    match event_type {
        TimelineEventType::Created => ("created".to_owned(), serde_json::json!({})),
        TimelineEventType::StateChanged { state } => (
            "state_changed".to_owned(),
            serde_json::json!({ "state": state }),
        ),
        TimelineEventType::LabelAdded { label } => (
            "label_added".to_owned(),
            serde_json::json!({ "label": label }),
        ),
        TimelineEventType::LabelRemoved { label } => (
            "label_removed".to_owned(),
            serde_json::json!({ "label": label }),
        ),
        TimelineEventType::MilestoneSet { milestone } => (
            "milestone_set".to_owned(),
            serde_json::json!({ "milestone": milestone }),
        ),
        TimelineEventType::MilestoneRemoved { milestone } => (
            "milestone_removed".to_owned(),
            serde_json::json!({ "milestone": milestone }),
        ),
        TimelineEventType::Merged => ("merged".to_owned(), serde_json::json!({})),
        TimelineEventType::NoteEvidence {
            note_id,
            snippet,
            discussion_id,
        } => (
            "note_evidence".to_owned(),
            serde_json::json!({
                "note_id": note_id,
                "snippet": snippet,
                "discussion_id": discussion_id,
            }),
        ),
        TimelineEventType::DiscussionThread {
            discussion_id,
            notes,
        } => (
            "discussion_thread".to_owned(),
            serde_json::json!({
                "discussion_id": discussion_id,
                "note_count": notes.len(),
                "notes": notes.iter().map(|n| serde_json::json!({
                    "note_id": n.note_id,
                    "author": n.author,
                    "body": n.body,
                    "created_at": ms_to_iso(n.created_at),
                })).collect::<Vec<_>>(),
            }),
        ),
        TimelineEventType::CrossReferenced { target } => (
            "cross_referenced".to_owned(),
            serde_json::json!({ "target": target }),
        ),
    }
}

#[derive(Serialize)]
struct TimelineMetaJson {
    search_mode: String,
    expansion_depth: u32,
    include_mentions: bool,
    total_entities: usize,
    total_events: usize,
    evidence_notes_included: usize,
    discussion_threads_included: usize,
    unresolved_references: usize,
    showing: usize,
}

fn count_evidence_notes(events: &[TimelineEvent]) -> usize {
    events
        .iter()
        .filter(|e| matches!(e.event_type, TimelineEventType::NoteEvidence { .. }))
        .count()
}

fn count_discussion_threads(events: &[TimelineEvent]) -> usize {
    events
        .iter()
        .filter(|e| matches!(e.event_type, TimelineEventType::DiscussionThread { .. }))
        .count()
}

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

    #[test]
    fn test_parse_issue_colon_number() {
        let q = parse_timeline_query("issue:42");
        assert!(
            matches!(q, TimelineQuery::EntityDirect { ref entity_type, iid } if entity_type == "issue" && iid == 42)
        );
    }

    #[test]
    fn test_parse_i_colon_number() {
        let q = parse_timeline_query("i:42");
        assert!(
            matches!(q, TimelineQuery::EntityDirect { ref entity_type, iid } if entity_type == "issue" && iid == 42)
        );
    }

    #[test]
    fn test_parse_mr_colon_number() {
        let q = parse_timeline_query("mr:99");
        assert!(
            matches!(q, TimelineQuery::EntityDirect { ref entity_type, iid } if entity_type == "merge_request" && iid == 99)
        );
    }

    #[test]
    fn test_parse_m_colon_number() {
        let q = parse_timeline_query("m:99");
        assert!(
            matches!(q, TimelineQuery::EntityDirect { ref entity_type, iid } if entity_type == "merge_request" && iid == 99)
        );
    }

    #[test]
    fn test_parse_case_insensitive() {
        let q = parse_timeline_query("ISSUE:42");
        assert!(
            matches!(q, TimelineQuery::EntityDirect { ref entity_type, iid } if entity_type == "issue" && iid == 42)
        );

        let q = parse_timeline_query("MR:99");
        assert!(
            matches!(q, TimelineQuery::EntityDirect { ref entity_type, iid } if entity_type == "merge_request" && iid == 99)
        );

        let q = parse_timeline_query("Issue:7");
        assert!(
            matches!(q, TimelineQuery::EntityDirect { ref entity_type, iid } if entity_type == "issue" && iid == 7)
        );
    }

    #[test]
    fn test_parse_search_fallback() {
        let q = parse_timeline_query("switch health");
        assert!(matches!(q, TimelineQuery::Search(ref s) if s == "switch health"));
    }

    #[test]
    fn test_parse_non_numeric_falls_back_to_search() {
        let q = parse_timeline_query("issue:abc");
        assert!(matches!(q, TimelineQuery::Search(_)));
    }

    #[test]
    fn test_parse_unknown_prefix_falls_back_to_search() {
        let q = parse_timeline_query("foo:42");
        assert!(matches!(q, TimelineQuery::Search(_)));
    }

    #[test]
    fn test_parse_whitespace_trimmed() {
        let q = parse_timeline_query("  issue:42  ");
        assert!(
            matches!(q, TimelineQuery::EntityDirect { ref entity_type, iid } if entity_type == "issue" && iid == 42)
        );
    }
}
