use crate::cli::render::{self, Align, StyledCell, Table as LoreTable, Theme};
use rusqlite::Connection;
use serde::Serialize;

use crate::Config;
use crate::cli::robot::{RobotMeta, expand_fields_preset, filter_fields};
use crate::core::error::{LoreError, Result};
use crate::core::path_resolver::escape_like as note_escape_like;
use crate::core::project::resolve_project;
use crate::core::time::{iso_to_ms, ms_to_iso, parse_since};

use super::render_helpers::{format_note_parent, format_note_path, format_note_type};

#[derive(Debug, Serialize)]
pub struct NoteListRow {
    pub id: i64,
    pub gitlab_id: i64,
    pub author_username: String,
    pub body: Option<String>,
    pub note_type: Option<String>,
    pub is_system: bool,
    pub created_at: i64,
    pub updated_at: i64,
    pub position_new_path: Option<String>,
    pub position_new_line: Option<i64>,
    pub position_old_path: Option<String>,
    pub position_old_line: Option<i64>,
    pub resolvable: bool,
    pub resolved: bool,
    pub resolved_by: Option<String>,
    pub noteable_type: Option<String>,
    pub parent_iid: Option<i64>,
    pub parent_title: Option<String>,
    pub project_path: String,
}

#[derive(Serialize)]
pub struct NoteListRowJson {
    pub id: i64,
    pub gitlab_id: i64,
    pub author_username: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub body: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub note_type: Option<String>,
    pub is_system: bool,
    pub created_at_iso: String,
    pub updated_at_iso: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub position_new_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub position_new_line: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub position_old_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub position_old_line: Option<i64>,
    pub resolvable: bool,
    pub resolved: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resolved_by: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub noteable_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_iid: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_title: Option<String>,
    pub project_path: String,
}

impl From<&NoteListRow> for NoteListRowJson {
    fn from(row: &NoteListRow) -> Self {
        Self {
            id: row.id,
            gitlab_id: row.gitlab_id,
            author_username: row.author_username.clone(),
            body: row.body.clone(),
            note_type: row.note_type.clone(),
            is_system: row.is_system,
            created_at_iso: ms_to_iso(row.created_at),
            updated_at_iso: ms_to_iso(row.updated_at),
            position_new_path: row.position_new_path.clone(),
            position_new_line: row.position_new_line,
            position_old_path: row.position_old_path.clone(),
            position_old_line: row.position_old_line,
            resolvable: row.resolvable,
            resolved: row.resolved,
            resolved_by: row.resolved_by.clone(),
            noteable_type: row.noteable_type.clone(),
            parent_iid: row.parent_iid,
            parent_title: row.parent_title.clone(),
            project_path: row.project_path.clone(),
        }
    }
}

#[derive(Debug)]
pub struct NoteListResult {
    pub notes: Vec<NoteListRow>,
    pub total_count: i64,
}

#[derive(Serialize)]
pub struct NoteListResultJson {
    pub notes: Vec<NoteListRowJson>,
    pub total_count: i64,
    pub showing: usize,
}

impl From<&NoteListResult> for NoteListResultJson {
    fn from(result: &NoteListResult) -> Self {
        Self {
            notes: result.notes.iter().map(NoteListRowJson::from).collect(),
            total_count: result.total_count,
            showing: result.notes.len(),
        }
    }
}

pub struct NoteListFilters {
    pub limit: usize,
    pub project: Option<String>,
    pub author: Option<String>,
    pub note_type: Option<String>,
    pub include_system: bool,
    pub for_issue_iid: Option<i64>,
    pub for_mr_iid: Option<i64>,
    pub note_id: Option<i64>,
    pub gitlab_note_id: Option<i64>,
    pub discussion_id: Option<String>,
    pub since: Option<String>,
    pub until: Option<String>,
    pub path: Option<String>,
    pub contains: Option<String>,
    pub resolution: Option<String>,
    pub sort: String,
    pub order: String,
}

pub fn print_list_notes(result: &NoteListResult) {
    if result.notes.is_empty() {
        println!("No notes found.");
        return;
    }

    println!(
        "{}  {} of {}\n",
        Theme::bold().render("Notes"),
        result.notes.len(),
        result.total_count
    );

    let mut table = LoreTable::new()
        .headers(&[
            "ID",
            "Author",
            "Type",
            "Body",
            "Path:Line",
            "Parent",
            "Created",
        ])
        .align(0, Align::Right)
        .flex_col(3);

    for note in &result.notes {
        let body = note
            .body
            .as_deref()
            .map(|b| b.replace('\n', " "))
            .unwrap_or_default();
        let path = format_note_path(note.position_new_path.as_deref(), note.position_new_line);
        let parent = format_note_parent(note.noteable_type.as_deref(), note.parent_iid);
        let relative_time = render::format_relative_time_compact(note.created_at);
        let note_type = format_note_type(note.note_type.as_deref());

        table.add_row(vec![
            StyledCell::styled(note.gitlab_id.to_string(), Theme::info()),
            StyledCell::styled(format!("@{}", note.author_username), Theme::accent()),
            StyledCell::plain(note_type),
            StyledCell::plain(body),
            StyledCell::plain(path),
            StyledCell::plain(parent),
            StyledCell::styled(relative_time, Theme::dim()),
        ]);
    }

    println!("{}", table.render());
}

pub fn print_list_notes_json(result: &NoteListResult, elapsed_ms: u64, fields: Option<&[String]>) {
    let json_result = NoteListResultJson::from(result);
    let meta = RobotMeta::new(elapsed_ms);
    let output = serde_json::json!({
        "ok": true,
        "data": json_result,
        "meta": meta,
    });
    let mut output = output;
    if let Some(f) = fields {
        let expanded = expand_fields_preset(f, "notes");
        filter_fields(&mut output, "notes", &expanded);
    }
    match serde_json::to_string(&output) {
        Ok(json) => println!("{json}"),
        Err(e) => eprintln!("Error serializing to JSON: {e}"),
    }
}

pub fn query_notes(
    conn: &Connection,
    filters: &NoteListFilters,
    config: &Config,
) -> Result<NoteListResult> {
    let mut where_clauses: Vec<String> = Vec::new();
    let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();

    if let Some(ref project) = filters.project {
        let project_id = resolve_project(conn, project)?;
        where_clauses.push("n.project_id = ?".to_string());
        params.push(Box::new(project_id));
    }

    if let Some(ref author) = filters.author {
        let username = author.strip_prefix('@').unwrap_or(author);
        where_clauses.push("n.author_username = ? COLLATE NOCASE".to_string());
        params.push(Box::new(username.to_string()));
    }

    if let Some(ref note_type) = filters.note_type {
        where_clauses.push("n.note_type = ?".to_string());
        params.push(Box::new(note_type.clone()));
    }

    if !filters.include_system {
        where_clauses.push("n.is_system = 0".to_string());
    }

    let since_ms = if let Some(ref since_str) = filters.since {
        let ms = parse_since(since_str).ok_or_else(|| {
            LoreError::Other(format!(
                "Invalid --since value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
                since_str
            ))
        })?;
        where_clauses.push("n.created_at >= ?".to_string());
        params.push(Box::new(ms));
        Some(ms)
    } else {
        None
    };

    if let Some(ref until_str) = filters.until {
        let until_ms = if until_str.len() == 10
            && until_str.chars().filter(|&c| c == '-').count() == 2
        {
            let iso_full = format!("{until_str}T23:59:59.999Z");
            iso_to_ms(&iso_full).ok_or_else(|| {
                LoreError::Other(format!(
                    "Invalid --until value '{}'. Use YYYY-MM-DD or relative format.",
                    until_str
                ))
            })?
        } else {
            parse_since(until_str).ok_or_else(|| {
                LoreError::Other(format!(
                    "Invalid --until value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
                    until_str
                ))
            })?
        };

        if let Some(s) = since_ms
            && s > until_ms
        {
            return Err(LoreError::Other(
                "Invalid time window: --since is after --until.".to_string(),
            ));
        }

        where_clauses.push("n.created_at <= ?".to_string());
        params.push(Box::new(until_ms));
    }

    if let Some(ref path) = filters.path {
        if let Some(prefix) = path.strip_suffix('/') {
            let escaped = note_escape_like(prefix);
            where_clauses.push("n.position_new_path LIKE ? ESCAPE '\\'".to_string());
            params.push(Box::new(format!("{escaped}%")));
        } else {
            where_clauses.push("n.position_new_path = ?".to_string());
            params.push(Box::new(path.clone()));
        }
    }

    if let Some(ref contains) = filters.contains {
        let escaped = note_escape_like(contains);
        where_clauses.push("n.body LIKE ? ESCAPE '\\' COLLATE NOCASE".to_string());
        params.push(Box::new(format!("%{escaped}%")));
    }

    if let Some(ref resolution) = filters.resolution {
        match resolution.as_str() {
            "unresolved" => {
                where_clauses.push("n.resolvable = 1 AND n.resolved = 0".to_string());
            }
            "resolved" => {
                where_clauses.push("n.resolvable = 1 AND n.resolved = 1".to_string());
            }
            other => {
                return Err(LoreError::Other(format!(
                    "Invalid --resolution value '{}'. Use 'resolved' or 'unresolved'.",
                    other
                )));
            }
        }
    }

    if let Some(iid) = filters.for_issue_iid {
        let project_str = filters
            .project
            .as_deref()
            .or(config.default_project.as_deref())
            .ok_or_else(|| {
                LoreError::Other(
                    "Cannot filter by issue IID without a project context. Use --project or set defaultProject in config."
                        .to_string(),
                )
            })?;
        let project_id = resolve_project(conn, project_str)?;
        where_clauses.push(
            "d.issue_id = (SELECT id FROM issues WHERE project_id = ? AND iid = ?)".to_string(),
        );
        params.push(Box::new(project_id));
        params.push(Box::new(iid));
    }

    if let Some(iid) = filters.for_mr_iid {
        let project_str = filters
            .project
            .as_deref()
            .or(config.default_project.as_deref())
            .ok_or_else(|| {
                LoreError::Other(
                    "Cannot filter by MR IID without a project context. Use --project or set defaultProject in config."
                        .to_string(),
                )
            })?;
        let project_id = resolve_project(conn, project_str)?;
        where_clauses.push(
            "d.merge_request_id = (SELECT id FROM merge_requests WHERE project_id = ? AND iid = ?)"
                .to_string(),
        );
        params.push(Box::new(project_id));
        params.push(Box::new(iid));
    }

    if let Some(id) = filters.note_id {
        where_clauses.push("n.id = ?".to_string());
        params.push(Box::new(id));
    }

    if let Some(gitlab_id) = filters.gitlab_note_id {
        where_clauses.push("n.gitlab_id = ?".to_string());
        params.push(Box::new(gitlab_id));
    }

    if let Some(ref disc_id) = filters.discussion_id {
        where_clauses.push("d.gitlab_discussion_id = ?".to_string());
        params.push(Box::new(disc_id.clone()));
    }

    let where_sql = if where_clauses.is_empty() {
        String::new()
    } else {
        format!("WHERE {}", where_clauses.join(" AND "))
    };

    let count_sql = format!(
        "SELECT COUNT(*) FROM notes n
         JOIN discussions d ON n.discussion_id = d.id
         JOIN projects p ON n.project_id = p.id
         LEFT JOIN issues i ON d.issue_id = i.id
         LEFT JOIN merge_requests m ON d.merge_request_id = m.id
         {where_sql}"
    );

    let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
    let total_count: i64 = conn.query_row(&count_sql, param_refs.as_slice(), |row| row.get(0))?;

    let sort_column = match filters.sort.as_str() {
        "updated" => "n.updated_at",
        _ => "n.created_at",
    };
    let order = if filters.order == "asc" {
        "ASC"
    } else {
        "DESC"
    };

    let query_sql = format!(
        "SELECT
            n.id,
            n.gitlab_id,
            n.author_username,
            n.body,
            n.note_type,
            n.is_system,
            n.created_at,
            n.updated_at,
            n.position_new_path,
            n.position_new_line,
            n.position_old_path,
            n.position_old_line,
            n.resolvable,
            n.resolved,
            n.resolved_by,
            d.noteable_type,
            COALESCE(i.iid, m.iid) AS parent_iid,
            COALESCE(i.title, m.title) AS parent_title,
            p.path_with_namespace AS project_path
         FROM notes n
         JOIN discussions d ON n.discussion_id = d.id
         JOIN projects p ON n.project_id = p.id
         LEFT JOIN issues i ON d.issue_id = i.id
         LEFT JOIN merge_requests m ON d.merge_request_id = m.id
         {where_sql}
         ORDER BY {sort_column} {order}, n.id {order}
         LIMIT ?"
    );

    params.push(Box::new(filters.limit as i64));
    let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();

    let mut stmt = conn.prepare(&query_sql)?;
    let notes: Vec<NoteListRow> = stmt
        .query_map(param_refs.as_slice(), |row| {
            let is_system_int: i64 = row.get(5)?;
            let resolvable_int: i64 = row.get(12)?;
            let resolved_int: i64 = row.get(13)?;

            Ok(NoteListRow {
                id: row.get(0)?,
                gitlab_id: row.get(1)?,
                author_username: row.get::<_, Option<String>>(2)?.unwrap_or_default(),
                body: row.get(3)?,
                note_type: row.get(4)?,
                is_system: is_system_int == 1,
                created_at: row.get(6)?,
                updated_at: row.get(7)?,
                position_new_path: row.get(8)?,
                position_new_line: row.get(9)?,
                position_old_path: row.get(10)?,
                position_old_line: row.get(11)?,
                resolvable: resolvable_int == 1,
                resolved: resolved_int == 1,
                resolved_by: row.get(14)?,
                noteable_type: row.get(15)?,
                parent_iid: row.get(16)?,
                parent_title: row.get(17)?,
                project_path: row.get(18)?,
            })
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(NoteListResult { notes, total_count })
}
