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

use crate::Config;
use crate::cli::robot::{expand_fields_preset, filter_fields};
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 super::render_helpers::{format_assignees, format_discussions};

#[derive(Debug, Serialize)]
pub struct IssueListRow {
    pub iid: i64,
    pub title: String,
    pub state: String,
    pub author_username: String,
    pub created_at: i64,
    pub updated_at: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub web_url: Option<String>,
    pub project_path: String,
    pub labels: Vec<String>,
    pub assignees: Vec<String>,
    pub discussion_count: i64,
    pub unresolved_count: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status_name: Option<String>,
    #[serde(skip_serializing)]
    pub status_category: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status_color: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status_icon_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status_synced_at: Option<i64>,
}

#[derive(Serialize)]
pub struct IssueListRowJson {
    pub iid: i64,
    pub title: String,
    pub state: String,
    pub author_username: String,
    pub labels: Vec<String>,
    pub assignees: Vec<String>,
    pub discussion_count: i64,
    pub unresolved_count: i64,
    pub created_at_iso: String,
    pub updated_at_iso: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub web_url: Option<String>,
    pub project_path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status_name: Option<String>,
    #[serde(skip_serializing)]
    pub status_category: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status_color: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status_icon_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status_synced_at_iso: Option<String>,
}

impl From<&IssueListRow> for IssueListRowJson {
    fn from(row: &IssueListRow) -> Self {
        Self {
            iid: row.iid,
            title: row.title.clone(),
            state: row.state.clone(),
            author_username: row.author_username.clone(),
            labels: row.labels.clone(),
            assignees: row.assignees.clone(),
            discussion_count: row.discussion_count,
            unresolved_count: row.unresolved_count,
            created_at_iso: ms_to_iso(row.created_at),
            updated_at_iso: ms_to_iso(row.updated_at),
            web_url: row.web_url.clone(),
            project_path: row.project_path.clone(),
            status_name: row.status_name.clone(),
            status_category: row.status_category.clone(),
            status_color: row.status_color.clone(),
            status_icon_name: row.status_icon_name.clone(),
            status_synced_at_iso: row.status_synced_at.map(ms_to_iso),
        }
    }
}

#[derive(Serialize)]
pub struct ListResult {
    pub issues: Vec<IssueListRow>,
    pub total_count: usize,
    pub available_statuses: Vec<String>,
}

#[derive(Serialize)]
pub struct ListResultJson {
    pub issues: Vec<IssueListRowJson>,
    pub total_count: usize,
    pub showing: usize,
}

impl From<&ListResult> for ListResultJson {
    fn from(result: &ListResult) -> Self {
        Self {
            issues: result.issues.iter().map(IssueListRowJson::from).collect(),
            total_count: result.total_count,
            showing: result.issues.len(),
        }
    }
}

pub struct ListFilters<'a> {
    pub limit: usize,
    pub project: Option<&'a str>,
    pub state: Option<&'a str>,
    pub author: Option<&'a str>,
    pub assignee: Option<&'a str>,
    pub labels: Option<&'a [String]>,
    pub milestone: Option<&'a str>,
    pub since: Option<&'a str>,
    pub due_before: Option<&'a str>,
    pub has_due_date: bool,
    pub statuses: &'a [String],
    pub sort: &'a str,
    pub order: &'a str,
}

pub fn run_list_issues(config: &Config, filters: ListFilters) -> Result<ListResult> {
    let db_path = get_db_path(config.storage.db_path.as_deref());
    let conn = create_connection(&db_path)?;

    let mut result = query_issues(&conn, &filters)?;
    result.available_statuses = query_available_statuses(&conn)?;
    Ok(result)
}

fn query_available_statuses(conn: &Connection) -> Result<Vec<String>> {
    let mut stmt = conn.prepare(
        "SELECT DISTINCT status_name FROM issues WHERE status_name IS NOT NULL ORDER BY status_name",
    )?;
    let statuses = stmt
        .query_map([], |row| row.get::<_, String>(0))?
        .collect::<std::result::Result<Vec<_>, _>>()?;
    Ok(statuses)
}

fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult> {
    let mut where_clauses = Vec::new();
    let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();

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

    if let Some(state) = filters.state
        && state != "all"
    {
        where_clauses.push("i.state = ?");
        params.push(Box::new(state.to_string()));
    }

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

    if let Some(assignee) = filters.assignee {
        let username = assignee.strip_prefix('@').unwrap_or(assignee);
        where_clauses.push(
            "EXISTS (SELECT 1 FROM issue_assignees ia
             WHERE ia.issue_id = i.id AND ia.username = ?)",
        );
        params.push(Box::new(username.to_string()));
    }

    if let Some(since_str) = filters.since {
        let cutoff_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("i.updated_at >= ?");
        params.push(Box::new(cutoff_ms));
    }

    if let Some(labels) = filters.labels {
        for label in labels {
            where_clauses.push(
                "EXISTS (SELECT 1 FROM issue_labels il
                 JOIN labels l ON il.label_id = l.id
                 WHERE il.issue_id = i.id AND l.name = ?)",
            );
            params.push(Box::new(label.clone()));
        }
    }

    if let Some(milestone) = filters.milestone {
        where_clauses.push("i.milestone_title = ?");
        params.push(Box::new(milestone.to_string()));
    }

    if let Some(due_before) = filters.due_before {
        where_clauses.push("i.due_date IS NOT NULL AND i.due_date <= ?");
        params.push(Box::new(due_before.to_string()));
    }

    if filters.has_due_date {
        where_clauses.push("i.due_date IS NOT NULL");
    }

    let status_in_clause;
    if filters.statuses.len() == 1 {
        where_clauses.push("i.status_name = ? COLLATE NOCASE");
        params.push(Box::new(filters.statuses[0].clone()));
    } else if filters.statuses.len() > 1 {
        let placeholders: Vec<&str> = filters.statuses.iter().map(|_| "?").collect();
        status_in_clause = format!(
            "i.status_name COLLATE NOCASE IN ({})",
            placeholders.join(", ")
        );
        where_clauses.push(&status_in_clause);
        for s in filters.statuses {
            params.push(Box::new(s.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 issues i
         JOIN projects p ON i.project_id = p.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 total_count = total_count as usize;

    let sort_column = match filters.sort {
        "created" => "i.created_at",
        "iid" => "i.iid",
        _ => "i.updated_at",
    };
    let order = if filters.order == "asc" {
        "ASC"
    } else {
        "DESC"
    };

    let query_sql = format!(
        "SELECT
            i.iid,
            i.title,
            i.state,
            i.author_username,
            i.created_at,
            i.updated_at,
            i.web_url,
            p.path_with_namespace,
            (SELECT GROUP_CONCAT(l.name, X'1F')
             FROM issue_labels il
             JOIN labels l ON il.label_id = l.id
             WHERE il.issue_id = i.id) AS labels_csv,
            (SELECT GROUP_CONCAT(ia.username, X'1F')
             FROM issue_assignees ia
             WHERE ia.issue_id = i.id) AS assignees_csv,
            (SELECT COUNT(*) FROM discussions d
             WHERE d.issue_id = i.id) AS discussion_count,
            (SELECT COUNT(*) FROM discussions d
             WHERE d.issue_id = i.id AND d.resolvable = 1 AND d.resolved = 0) AS unresolved_count,
            i.status_name,
            i.status_category,
            i.status_color,
            i.status_icon_name,
            i.status_synced_at
         FROM issues i
         JOIN projects p ON i.project_id = p.id
         {where_sql}
         ORDER BY {sort_column} {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 issues: Vec<IssueListRow> = stmt
        .query_map(param_refs.as_slice(), |row| {
            let labels_csv: Option<String> = row.get(8)?;
            let labels = labels_csv
                .map(|s| s.split('\x1F').map(String::from).collect())
                .unwrap_or_default();

            let assignees_csv: Option<String> = row.get(9)?;
            let assignees = assignees_csv
                .map(|s| s.split('\x1F').map(String::from).collect())
                .unwrap_or_default();

            Ok(IssueListRow {
                iid: row.get(0)?,
                title: row.get(1)?,
                state: row.get(2)?,
                author_username: row.get(3)?,
                created_at: row.get(4)?,
                updated_at: row.get(5)?,
                web_url: row.get(6)?,
                project_path: row.get(7)?,
                labels,
                assignees,
                discussion_count: row.get(10)?,
                unresolved_count: row.get(11)?,
                status_name: row.get(12)?,
                status_category: row.get(13)?,
                status_color: row.get(14)?,
                status_icon_name: row.get(15)?,
                status_synced_at: row.get(16)?,
            })
        })?
        .collect::<std::result::Result<Vec<_>, _>>()?;

    Ok(ListResult {
        issues,
        total_count,
        available_statuses: Vec::new(),
    })
}

pub fn print_list_issues(result: &ListResult) {
    if result.issues.is_empty() {
        println!("No issues found.");
        return;
    }

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

    let has_any_status = result.issues.iter().any(|i| i.status_name.is_some());

    let mut headers = vec!["IID", "Title", "State"];
    if has_any_status {
        headers.push("Status");
    }
    headers.extend(["Assignee", "Labels", "Disc", "Updated"]);

    let mut table = LoreTable::new()
        .headers(&headers)
        .align(0, Align::Right)
        .flex_col(1);

    for issue in &result.issues {
        let title = issue.title.clone();
        let relative_time = render::format_relative_time_compact(issue.updated_at);
        let labels = render::format_labels_bare(&issue.labels, 2);
        let assignee = format_assignees(&issue.assignees);
        let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);

        let (icon, state_style) = if issue.state == "opened" {
            (Icons::issue_opened(), Theme::success())
        } else {
            (Icons::issue_closed(), Theme::dim())
        };
        let state_cell = StyledCell::styled(format!("{icon} {}", issue.state), state_style);

        let mut row = vec![
            StyledCell::styled(format!("#{}", issue.iid), Theme::info()),
            StyledCell::plain(title),
            state_cell,
        ];
        if has_any_status {
            match &issue.status_name {
                Some(status) => {
                    row.push(StyledCell::plain(render::style_with_hex(
                        status,
                        issue.status_color.as_deref(),
                    )));
                }
                None => {
                    row.push(StyledCell::plain(""));
                }
            }
        }
        row.extend([
            StyledCell::styled(assignee, Theme::accent()),
            StyledCell::styled(labels, Theme::warning()),
            discussions,
            StyledCell::styled(relative_time, Theme::dim()),
        ]);
        table.add_row(row);
    }

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

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

pub fn open_issue_in_browser(result: &ListResult) -> Option<String> {
    let first_issue = result.issues.first()?;
    let url = first_issue.web_url.as_ref()?;

    match open::that(url) {
        Ok(()) => {
            println!("Opened: {url}");
            Some(url.clone())
        }
        Err(e) => {
            eprintln!("Failed to open browser: {e}");
            None
        }
    }
}
