use anyhow::Result;
use colored::Colorize;
use comfy_table::presets::UTF8_FULL;
use comfy_table::{Cell, ContentArrangement, Table};

use crate::api::{
    AssigneeFilter, FilteredWorkItemQuery, PlaneApiClient, PlaneState, PlaneWorkItem,
    ScheduledDate, StateGroup, WorkItemPriority,
};
use crate::config::{ProjectScope, resolve_plane_session};
use crate::terminal_ui::OutputFormat;

/// Fetches and displays work items from the active Plane workspace and project.
/// Resolves the configured project to an API UUID, fetches states for display-name
/// resolution, then renders work items in the selected output format (table/json/csv).
/// Supports client-side filtering by state name, priority level, label name, and
/// overdue status, plus server-side filtering by assignee.
#[derive(clap::Args)]
pub struct ListWorkItemsArgs {
    /// Project slug to query (overrides the configured default project)
    #[arg(long)]
    project: Option<String>,
    /// Output format: table (default for TTY), json (default for pipes), csv
    #[arg(long)]
    format: Option<String>,
    /// Filter by state name (case-insensitive, e.g. "todo", "in progress", "done")
    #[arg(long)]
    state: Option<String>,
    /// Filter by priority level: urgent, high, medium, low, none
    #[arg(long)]
    priority: Option<String>,
    /// Filter by label name (case-insensitive substring match)
    #[arg(long)]
    label: Option<String>,
    /// Filter by assignee: "me" for the current user, or a member UUID
    #[arg(long)]
    assignee: Option<String>,
    /// Filter to items in a specific module (case-insensitive name match)
    #[arg(long)]
    module: Option<String>,
    /// Filter to items in a specific cycle (case-insensitive name match)
    #[arg(long)]
    cycle: Option<String>,
    /// Only show items that are overdue (target_date < today, not completed/cancelled)
    #[arg(long)]
    overdue: bool,
}

/// Execution logic for the `plane ls` subcommand
impl ListWorkItemsArgs {
    /// Resolves the target project, fetches states and work items from the Plane API,
    /// and renders matching work items as a colored table, JSON, or CSV
    pub async fn execute(self) -> Result<()> {
        let plane_session = resolve_plane_session()?;
        let api_client = PlaneApiClient::try_new(plane_session)?;

        let target_project_slug = match self.project {
            Option::Some(ref explicit_project) => explicit_project.parse()?,
            Option::None => match api_client.project_scope() {
                ProjectScope::Scoped(default_project) => default_project.as_str().parse()?,
                ProjectScope::ExplicitOnly => {
                    anyhow::bail!(
                        "no default project configured — pass --project or run: \
                         plane config --project <slug>"
                    );
                }
            },
        };

        let resolved_project = api_client
            .find_project_by_slug(&target_project_slug)
            .await?;
        let project_identifier_code = resolved_project.identifier().to_owned();
        let resolved_project_id = resolved_project.id().clone();

        let project_states = api_client.list_states(&resolved_project_id).await?;

        // Resolve assignee filter for server-side filtering
        let assignee_filter = match self.assignee {
            Option::Some(ref assignee_text) if assignee_text.eq_ignore_ascii_case("me") => {
                let my_member_id = api_client.current_user_member_id().await?;
                AssigneeFilter::ByMember(my_member_id)
            }
            Option::Some(ref assignee_uuid) => {
                let member_id = assignee_uuid.parse()?;
                AssigneeFilter::ByMember(member_id)
            }
            Option::None => AssigneeFilter::Unfiltered,
        };

        // Fetch work items: from module, cycle, or full project (with optional assignee filter)
        let all_work_items = match (&self.module, &self.cycle) {
            (Option::Some(module_name), Option::None) => {
                let modules = api_client.list_modules(&resolved_project_id).await?;
                let target_module = modules
                    .iter()
                    .find(|m| m.display_name().eq_ignore_ascii_case(module_name));
                match target_module {
                    Option::Some(found_module) => {
                        let project_client = api_client.project_operations(&resolved_project_id);
                        let module_client = project_client.module_operations(found_module.id());
                        module_client.list_work_items().await?
                    }
                    Option::None => {
                        let available_names: Vec<&str> =
                            modules.iter().map(|m| m.display_name()).collect();
                        anyhow::bail!(
                            "module '{}' not found — available: {}",
                            module_name,
                            available_names.join(", ")
                        );
                    }
                }
            }
            (Option::None, Option::Some(cycle_name)) => {
                let cycles = api_client.list_cycles(&resolved_project_id).await?;
                let target_cycle = cycles
                    .iter()
                    .find(|c| c.display_name().eq_ignore_ascii_case(cycle_name));
                match target_cycle {
                    Option::Some(found_cycle) => {
                        let project_client = api_client.project_operations(&resolved_project_id);
                        let cycle_client = project_client.cycle_operations(found_cycle.id());
                        cycle_client.list_work_items().await?
                    }
                    Option::None => {
                        let available_names: Vec<&str> =
                            cycles.iter().map(|c| c.display_name()).collect();
                        anyhow::bail!(
                            "cycle '{}' not found — available: {}",
                            cycle_name,
                            available_names.join(", ")
                        );
                    }
                }
            }
            (Option::Some(_), Option::Some(_)) => {
                anyhow::bail!("cannot combine --module and --cycle filters");
            }
            (Option::None, Option::None) => {
                let work_item_filter_query =
                    FilteredWorkItemQuery::for_project(resolved_project_id.clone())
                        .with_assignee_filter(assignee_filter);
                api_client.list_work_items_filtered(&work_item_filter_query).await?
            }
        };

        // Resolve label filter: name -> LabelId
        let resolved_label_id = match self.label {
            Option::Some(ref label_name) => {
                let labels = api_client.list_labels(&resolved_project_id).await?;
                let target_label = labels
                    .iter()
                    .find(|l| l.display_name().eq_ignore_ascii_case(label_name));
                match target_label {
                    Option::Some(found_label) => Option::Some(found_label.id().clone()),
                    Option::None => {
                        let available_names: Vec<&str> =
                            labels.iter().map(|l| l.display_name()).collect();
                        anyhow::bail!(
                            "label '{}' not found — available: {}",
                            label_name,
                            available_names.join(", ")
                        );
                    }
                }
            }
            Option::None => Option::None,
        };

        let today_date_string = today_iso8601();

        let work_items: Vec<&PlaneWorkItem> =
            all_work_items.iter().filter(|work_item| {
                if let Option::Some(ref state_filter) = self.state {
                    let mut item_state_name = work_item.workflow_state_id().to_string();
                    for candidate_state in &project_states {
                        if candidate_state.id() == work_item.workflow_state_id() {
                            item_state_name = candidate_state.display_name().to_owned();
                        }
                    }
                    if !item_state_name.eq_ignore_ascii_case(state_filter) {
                        return false;
                    }
                }
                if let Option::Some(ref priority_filter) = self.priority {
                    let item_priority_label = format!("{:?}", work_item.priority());
                    if !item_priority_label.eq_ignore_ascii_case(priority_filter) {
                        return false;
                    }
                }
                // Label filter: check if any of the item's label IDs match the resolved label
                if let Option::Some(ref target_label_id) = resolved_label_id {
                    let has_matching_label = work_item
                        .label_ids()
                        .iter()
                        .any(|item_label_id| item_label_id == target_label_id);
                    if !has_matching_label {
                        return false;
                    }
                }
                // Overdue filter: target_date < today AND state group not completed/cancelled
                if self.overdue {
                    let is_overdue = match work_item.target_date() {
                        ScheduledDate::Scheduled(date_text) => date_text.as_str() < today_date_string.as_str(),
                        ScheduledDate::Unscheduled => false,
                    };
                    if !is_overdue {
                        return false;
                    }
                    let item_state_group = find_state_group(work_item, &project_states);
                    match item_state_group {
                        Option::Some(StateGroup::Completed) | Option::Some(StateGroup::Cancelled) => {
                            return false;
                        }
                        _ => {}
                    }
                }
                true
            }).collect();

        let selected_format = match &self.format {
            Option::Some(format_text) => format_text.parse()?,
            Option::None => OutputFormat::detect_from_terminal(),
        };

        let work_item_renderer = WorkItemListRenderer {
            project_states: &project_states,
            project_identifier_code: &project_identifier_code,
        };

        match selected_format {
            OutputFormat::Table => work_item_renderer.print_table(&work_items),
            OutputFormat::Json => work_item_renderer.print_json(&work_items),
            OutputFormat::Csv => work_item_renderer.print_csv(&work_items),
        }

        Ok(())
    }
}

/// Returns today's date as an ISO 8601 string (YYYY-MM-DD) for overdue comparison.
/// Uses chrono-free string formatting via the system clock.
fn today_iso8601() -> String {
    let now = std::time::SystemTime::now();
    let duration_since_epoch = match now.duration_since(std::time::UNIX_EPOCH) {
        Ok(duration) => duration,
        Err(_) => return String::from("1970-01-01"),
    };
    let total_days = duration_since_epoch.as_secs() / 86400;
    // Convert days since epoch to YYYY-MM-DD using a simple civil date calculation
    let (year, month, day) = days_to_civil(total_days as i64);
    format!("{:04}-{:02}-{:02}", year, month, day)
}

/// Converts days since Unix epoch (1970-01-01) to a civil calendar date.
/// Algorithm from Howard Hinnant's date algorithms (public domain).
fn days_to_civil(days_since_epoch: i64) -> (i64, u32, u32) {
    let z = days_since_epoch + 719468;
    let era = z.wrapping_div(146097);
    let doe = (z - era * 146097) as u64;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = (yoe as i64) + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let adjusted_year = if m <= 2 { y + 1 } else { y };
    (adjusted_year, m as u32, d as u32)
}

/// Finds the state group for a work item by matching its state ID against the project states
fn find_state_group(work_item: &PlaneWorkItem, project_states: &[PlaneState]) -> Option<StateGroup> {
    for candidate_state in project_states {
        if candidate_state.id() == work_item.workflow_state_id() {
            return Option::Some(candidate_state.state_group());
        }
    }
    Option::None
}

/// Holds the project-scoped context (states and identifier code) needed to render
/// work item lists, avoiding repeated parameter passing across rendering methods
pub struct WorkItemListRenderer<'a> {
    /// The project's workflow state definitions for resolving state UUIDs to display names
    pub project_states: &'a [PlaneState],
    /// The project identifier code (e.g. "NRO") for constructing human-readable item IDs
    pub project_identifier_code: &'a str,
}

/// Rendering methods that produce table, JSON, and CSV output for work item lists,
/// resolving state UUIDs to display names via linear search over the project's state definitions
impl<'a> WorkItemListRenderer<'a> {
    /// Renders work items as a colored terminal table with ID, title, state, priority, and due columns
    pub fn print_table(&self, work_items: &[&PlaneWorkItem]) {
        let mut terminal_table = Table::new();
        terminal_table
            .load_preset(UTF8_FULL)
            .set_content_arrangement(ContentArrangement::Dynamic)
            .set_header(vec![
                Cell::new("ID"),
                Cell::new("Title"),
                Cell::new("State"),
                Cell::new("Priority"),
                Cell::new("Due"),
            ]);

        for work_item in work_items {
            let identifier_cell = format!(
                "{}-{}",
                self.project_identifier_code,
                work_item.sequence_id().as_u32()
            );

            let state_cell = self.resolve_state_display(work_item);
            let priority_cell = format_priority_display(work_item.priority());

            let due_cell = match work_item.target_date() {
                ScheduledDate::Scheduled(date_text) => date_text.to_owned(),
                ScheduledDate::Unscheduled => String::from("—"),
            };

            terminal_table.add_row(vec![
                Cell::new(identifier_cell),
                Cell::new(work_item.title().as_str()),
                Cell::new(state_cell),
                Cell::new(priority_cell),
                Cell::new(due_cell),
            ]);
        }

        println!("{}", terminal_table);
    }

    /// Renders work items as pretty-printed JSON array for piping and programmatic consumption
    pub fn print_json(&self, work_items: &[&PlaneWorkItem]) {
        let json_items: Vec<serde_json::Value> = work_items
            .iter()
            .map(|work_item| {
                let state_display = self.find_state_name(work_item);
                serde_json::json!({
                    "id": format!("{}-{}", self.project_identifier_code, work_item.sequence_id().as_u32()),
                    "title": work_item.title().as_str(),
                    "state": state_display,
                    "priority": format!("{:?}", work_item.priority()),
                    "due": work_item.target_date().as_date_string(),
                })
            })
            .collect();

        match serde_json::to_string_pretty(&json_items) {
            Ok(formatted_json) => println!("{}", formatted_json),
            Err(serialization_error) => {
                eprintln!("error serializing JSON: {}", serialization_error);
            }
        }
    }

    /// Renders work items as CSV rows with header for spreadsheet import
    pub fn print_csv(&self, work_items: &[&PlaneWorkItem]) {
        println!("id,title,state,priority,due");

        for work_item in work_items {
            let identifier_field = format!(
                "{}-{}",
                self.project_identifier_code,
                work_item.sequence_id().as_u32()
            );

            let state_field = self.find_state_name(work_item);

            let due_field = match work_item.target_date().as_date_string() {
                Option::Some(date_text) => date_text.to_owned(),
                Option::None => String::new(),
            };

            println!(
                "{},\"{}\",{},{:?},{}",
                identifier_field,
                work_item.title().as_str().replace('"', "\"\""),
                state_field,
                work_item.priority(),
                due_field,
            );
        }
    }

    fn resolve_state_display(&self, target_work_item: &PlaneWorkItem) -> String {
        for candidate_state in self.project_states {
            if candidate_state.id() == target_work_item.workflow_state_id() {
                return colorize_state_display(candidate_state);
            }
        }
        target_work_item.workflow_state_id().to_string()
    }

    fn find_state_name(&self, target_work_item: &PlaneWorkItem) -> String {
        for candidate_state in self.project_states {
            if candidate_state.id() == target_work_item.workflow_state_id() {
                return candidate_state.display_name().to_owned();
            }
        }
        target_work_item.workflow_state_id().to_string()
    }
}

fn colorize_state_display(resolved_state: &PlaneState) -> String {
    match resolved_state.state_group() {
        StateGroup::Backlog => resolved_state.display_name().dimmed().to_string(),
        StateGroup::Unstarted => resolved_state.display_name().white().to_string(),
        StateGroup::Started => resolved_state.display_name().yellow().to_string(),
        StateGroup::Completed => resolved_state.display_name().green().to_string(),
        StateGroup::Cancelled => resolved_state.display_name().red().to_string(),
    }
}

fn format_priority_display(work_item_priority: WorkItemPriority) -> String {
    match work_item_priority {
        WorkItemPriority::Urgent => "Urgent".red().bold().to_string(),
        WorkItemPriority::High => "High".red().to_string(),
        WorkItemPriority::Medium => "Medium".yellow().to_string(),
        WorkItemPriority::Low => "Low".dimmed().to_string(),
        WorkItemPriority::None => String::from("—"),
    }
}
