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

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

/// Entry point for the `plane cycles` subcommand tree, dispatching to list, create,
/// delete, add, or remove operations on project cycles.
/// Usage: `plane cycles ls`, `plane cycles create "Sprint 5" --start 2026-04-01 --end 2026-04-15`,
///        `plane cycles add NRO-123 "Sprint 4"`
#[derive(clap::Args)]
pub struct CycleCommand {
    /// The cycle action to perform
    #[command(subcommand)]
    action: CycleCommandAction,
}

/// Available cycle subcommands: listing, creating, deleting, and managing work items
#[derive(clap::Subcommand)]
enum CycleCommandAction {
    /// List all cycles or items in a named cycle
    Ls(CycleCommandListArgs),
    /// Create a new cycle with start and end dates
    Create(CycleCommandCreateArgs),
    /// Delete a cycle
    Delete(CycleCommandDeleteArgs),
    /// Add a work item to a cycle
    Add(CycleCommandAddArgs),
    /// Remove a work item from a cycle
    Remove(CycleCommandRemoveArgs),
}

/// Top-level dispatch for the cycle command tree
impl CycleCommand {
    /// Routes to the appropriate cycle subcommand based on the parsed CLI action
    pub async fn execute(self) -> Result<()> {
        match self.action {
            CycleCommandAction::Ls(list_args) => list_args.execute().await,
            CycleCommandAction::Create(create_args) => create_args.execute().await,
            CycleCommandAction::Delete(delete_args) => delete_args.execute().await,
            CycleCommandAction::Add(add_args) => add_args.execute().await,
            CycleCommandAction::Remove(remove_args) => remove_args.execute().await,
        }
    }
}

/// Arguments for listing all cycles or work items within a named cycle.
/// Without a cycle name: shows all cycles with date ranges.
/// With a cycle name: shows work items assigned to that cycle.
/// Usage: `plane cycles ls` or `plane cycles ls "Sprint 4"`
#[derive(clap::Args)]
struct CycleCommandListArgs {
    /// Cycle name to list items from (omit to list all cycles)
    cycle_name: Option<String>,
    /// Project slug to query (overrides the configured default)
    #[arg(long)]
    project: Option<String>,
    /// Output format: table (default for TTY), json (default for pipes), csv
    #[arg(long)]
    format: Option<String>,
    /// Open the cycle or cycles list in the default browser instead of printing
    #[arg(long)]
    web: Option<String>,
}

/// Dispatches cycle listing: all cycles overview or work items within a named cycle
impl CycleCommandListArgs {
    /// Either lists all cycles or lists work items in a specific named cycle
    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?;

        match self.web {
            Option::Some(_) => {
                let workspace_slug = api_client.workspace_slug();
                match self.cycle_name {
                    Option::None => {
                        let mut browser_url = String::with_capacity(128);
                        browser_url.push_str("https://app.plane.so/");
                        browser_url.push_str(workspace_slug.as_str());
                        browser_url.push_str("/projects/");
                        browser_url.push_str(resolved_project.id().as_str());
                        browser_url.push_str("/cycles/");
                        super::open::launch_browser(&browser_url)?;
                        println!("Opened cycles in browser");
                    }
                    Option::Some(ref target_cycle_name) => {
                        let project_cycles =
                            api_client.list_cycles(resolved_project.id()).await?;
                        let mut matched_cycle_id = Option::None;
                        for candidate_cycle in &project_cycles {
                            if candidate_cycle
                                .display_name()
                                .eq_ignore_ascii_case(target_cycle_name)
                            {
                                matched_cycle_id =
                                    Option::Some(candidate_cycle.id().as_str().to_owned());
                            }
                        }
                        let resolved_cycle_uuid = match matched_cycle_id {
                            Option::Some(cycle_uuid) => cycle_uuid,
                            Option::None => {
                                let available: Vec<&str> = project_cycles
                                    .iter()
                                    .map(|c| c.display_name())
                                    .collect();
                                anyhow::bail!(
                                    "cycle '{}' not found — available: {}",
                                    target_cycle_name,
                                    available.join(", ")
                                );
                            }
                        };
                        let mut browser_url = String::with_capacity(128);
                        browser_url.push_str("https://app.plane.so/");
                        browser_url.push_str(workspace_slug.as_str());
                        browser_url.push_str("/projects/");
                        browser_url.push_str(resolved_project.id().as_str());
                        browser_url.push_str("/cycles/");
                        browser_url.push_str(&resolved_cycle_uuid);
                        super::open::launch_browser(&browser_url)?;
                        println!("Opened cycle {} in browser", target_cycle_name);
                    }
                }
                return Ok(());
            }
            Option::None => {}
        }

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

        match self.cycle_name {
            Option::None => {
                let project_cycles = api_client.list_cycles(resolved_project.id()).await?;
                match selected_format {
                    OutputFormat::Table => render_cycles_table(&project_cycles),
                    OutputFormat::Json => render_cycles_json(&project_cycles),
                    OutputFormat::Csv => render_cycles_csv(&project_cycles),
                }
            }
            Option::Some(ref target_cycle_name) => {
                let project_cycles = api_client.list_cycles(resolved_project.id()).await?;
                let mut matched_cycle_id = Option::None;
                for candidate_cycle in &project_cycles {
                    if candidate_cycle.display_name().eq_ignore_ascii_case(target_cycle_name) {
                        matched_cycle_id = Option::Some(candidate_cycle.id());
                    }
                }
                let resolved_cycle_id = match matched_cycle_id {
                    Option::Some(cycle_id) => cycle_id,
                    Option::None => {
                        let available: Vec<&str> = project_cycles
                            .iter()
                            .map(|c| c.display_name())
                            .collect();
                        anyhow::bail!(
                            "cycle '{}' not found — available: {}",
                            target_cycle_name,
                            available.join(", ")
                        );
                    }
                };

                let project_client = api_client.project_operations(resolved_project.id());
                let cycle_client = project_client.cycle_operations(resolved_cycle_id);
                let cycle_work_items = cycle_client.list_work_items().await?;

                let project_states = api_client.list_states(resolved_project.id()).await?;
                let project_identifier_code = resolved_project.identifier();

                let cycle_command_item_renderer = CycleCommandItemRenderer {
                    project_states: &project_states,
                    project_identifier_code,
                };

                let item_refs: Vec<&PlaneWorkItem> = cycle_work_items.iter().collect();
                match selected_format {
                    OutputFormat::Table => cycle_command_item_renderer.print_table(&item_refs),
                    OutputFormat::Json => cycle_command_item_renderer.print_json(&item_refs),
                    OutputFormat::Csv => cycle_command_item_renderer.print_csv(&item_refs),
                }
            }
        }

        Ok(())
    }
}

/// Arguments for creating a new cycle in the active project
#[derive(clap::Args)]
struct CycleCommandCreateArgs {
    /// Display name for the new cycle (e.g. "Sprint 5")
    cycle_display_name: String,
    /// Start date in YYYY-MM-DD format
    #[arg(long)]
    start: String,
    /// End date in YYYY-MM-DD format
    #[arg(long)]
    end: String,
    /// Project slug to target (overrides the configured default)
    #[arg(long)]
    project: Option<String>,
}

/// Builds and sends the cycle creation request to the Plane API
impl CycleCommandCreateArgs {
    /// Creates a new cycle in the resolved project with start and end dates
    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_client = api_client.project_operations(resolved_project.id());
        project_client
            .create_cycle(CreateCycleParams {
                cycle_name: &self.cycle_display_name,
                start_date: &self.start,
                end_date: &self.end,
            })
            .await?;

        println!("Created cycle '{}'", self.cycle_display_name);
        Ok(())
    }
}

/// Arguments for deleting a cycle from the active project
#[derive(clap::Args)]
struct CycleCommandDeleteArgs {
    /// Display name of the cycle to delete (case-insensitive match)
    cycle_display_name: String,
    /// Project slug to target (overrides the configured default)
    #[arg(long)]
    project: Option<String>,
}

/// Resolves a cycle by name and sends the deletion request to the Plane API
impl CycleCommandDeleteArgs {
    /// Resolves the cycle by name and deletes it
    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_cycles = api_client.list_cycles(resolved_project.id()).await?;
        let resolved_cycle_id =
            resolve_cycle_command_target(CycleCommandResolveParams {
                project_cycles: &project_cycles,
                target_name: &self.cycle_display_name,
            })?;

        let project_client = api_client.project_operations(resolved_project.id());
        project_client.delete_cycle(resolved_cycle_id).await?;

        println!("Deleted cycle '{}'", self.cycle_display_name);
        Ok(())
    }
}

/// Arguments for adding a work item to a cycle
#[derive(clap::Args)]
struct CycleCommandAddArgs {
    /// Work item identifier (e.g. "NRO-123")
    identifier: String,
    /// Name of the cycle to add to (case-insensitive match)
    cycle_name: String,
    /// Project slug to target (overrides the configured default)
    #[arg(long)]
    project: Option<String>,
}

/// Resolves both the work item and cycle, then links them via the Plane API
impl CycleCommandAddArgs {
    /// Resolves the work item and cycle, then adds the item to the cycle
    async fn execute(self) -> Result<()> {
        let plane_session = resolve_plane_session()?;
        let api_client = PlaneApiClient::try_new(plane_session)?;

        let work_item_identifier: WorkItemIdentifier = self.identifier.parse()?;
        let resolved_work_item = api_client
            .find_work_item_by_identifier(&work_item_identifier)
            .await?;

        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_cycles = api_client.list_cycles(resolved_project.id()).await?;
        let resolved_cycle_id =
            resolve_cycle_command_target(CycleCommandResolveParams {
                project_cycles: &project_cycles,
                target_name: &self.cycle_name,
            })?;

        let project_client = api_client.project_operations(resolved_project.id());
        let cycle_client = project_client.cycle_operations(resolved_cycle_id);
        cycle_client.add_work_item(resolved_work_item.id()).await?;

        println!(
            "Added {} to cycle '{}'",
            work_item_identifier, self.cycle_name
        );
        Ok(())
    }
}

/// Arguments for removing a work item from a cycle
#[derive(clap::Args)]
struct CycleCommandRemoveArgs {
    /// Work item identifier (e.g. "NRO-123")
    identifier: String,
    /// Name of the cycle to remove from (case-insensitive match)
    cycle_name: String,
    /// Project slug to target (overrides the configured default)
    #[arg(long)]
    project: Option<String>,
}

/// Resolves both the work item and cycle, then unlinks them via the Plane API
impl CycleCommandRemoveArgs {
    /// Resolves the work item and cycle, then removes the item from the cycle
    async fn execute(self) -> Result<()> {
        let plane_session = resolve_plane_session()?;
        let api_client = PlaneApiClient::try_new(plane_session)?;

        let work_item_identifier: WorkItemIdentifier = self.identifier.parse()?;
        let resolved_work_item = api_client
            .find_work_item_by_identifier(&work_item_identifier)
            .await?;

        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_cycles = api_client.list_cycles(resolved_project.id()).await?;
        let resolved_cycle_id =
            resolve_cycle_command_target(CycleCommandResolveParams {
                project_cycles: &project_cycles,
                target_name: &self.cycle_name,
            })?;

        let project_client = api_client.project_operations(resolved_project.id());
        let cycle_client = project_client.cycle_operations(resolved_cycle_id);
        cycle_client
            .remove_work_item(resolved_work_item.id())
            .await?;

        println!(
            "Removed {} from cycle '{}'",
            work_item_identifier, self.cycle_name
        );
        Ok(())
    }
}

/// Parameters for resolving a cycle by name within a project's cycle list
struct CycleCommandResolveParams<'a> {
    /// The full list of cycles in the target project
    project_cycles: &'a [crate::api::PlaneCycle],
    /// The display name to match (case-insensitive)
    target_name: &'a str,
}

/// Case-insensitive cycle name resolution against the project's cycle inventory.
/// Returns the matched cycle ID or bails with the list of available cycle names.
fn resolve_cycle_command_target<'a>(
    params: CycleCommandResolveParams<'a>,
) -> Result<&'a crate::api::CycleId> {
    let mut matched_cycle_id = Option::None;
    for candidate_cycle in params.project_cycles {
        if candidate_cycle
            .display_name()
            .eq_ignore_ascii_case(params.target_name)
        {
            matched_cycle_id = Option::Some(candidate_cycle.id());
        }
    }
    match matched_cycle_id {
        Option::Some(cycle_id) => Ok(cycle_id),
        Option::None => {
            let available: Vec<&str> = params
                .project_cycles
                .iter()
                .map(|c| c.display_name())
                .collect();
            anyhow::bail!(
                "cycle '{}' not found — available: {}",
                params.target_name,
                available.join(", ")
            );
        }
    }
}

/// Holds project context needed to render cycle work items with state colors and identifiers
struct CycleCommandItemRenderer<'a> {
    project_states: &'a [PlaneState],
    project_identifier_code: &'a str,
}

/// Table, JSON, and CSV output for work items scoped to a specific cycle
impl<'a> CycleCommandItemRenderer<'a> {
    fn print_table(&self, work_items: &[&PlaneWorkItem]) {
        if work_items.is_empty() {
            println!("No items in this cycle.");
            return;
        }

        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);
    }

    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),
        }
    }

    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 render_cycles_table(project_cycles: &[crate::api::PlaneCycle]) {
    if project_cycles.is_empty() {
        println!("No cycles found.");
        return;
    }

    let mut terminal_table = Table::new();
    terminal_table
        .load_preset(UTF8_FULL)
        .set_content_arrangement(ContentArrangement::Dynamic)
        .set_header(vec![
            Cell::new("Cycle"),
            Cell::new("Start"),
            Cell::new("End"),
        ]);

    for cycle_entry in project_cycles {
        let start_cell = match cycle_entry.start_date() {
            "" => String::from("—"),
            date_text => date_text.to_owned(),
        };
        let end_cell = match cycle_entry.end_date() {
            "" => String::from("—"),
            date_text => date_text.to_owned(),
        };

        terminal_table.add_row(vec![
            Cell::new(cycle_entry.display_name()),
            Cell::new(start_cell),
            Cell::new(end_cell),
        ]);
    }

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

fn render_cycles_json(project_cycles: &[crate::api::PlaneCycle]) {
    let json_entries: Vec<serde_json::Value> = project_cycles
        .iter()
        .map(|cycle_entry| {
            let start_date = match cycle_entry.start_date() {
                "" => serde_json::Value::Null,
                date_text => serde_json::Value::String(date_text.to_owned()),
            };
            let end_date = match cycle_entry.end_date() {
                "" => serde_json::Value::Null,
                date_text => serde_json::Value::String(date_text.to_owned()),
            };
            serde_json::json!({
                "id": cycle_entry.id().to_string(),
                "name": cycle_entry.display_name(),
                "start_date": start_date,
                "end_date": end_date,
            })
        })
        .collect();

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

fn render_cycles_csv(project_cycles: &[crate::api::PlaneCycle]) {
    println!("id,name,start_date,end_date");
    for cycle_entry in project_cycles {
        println!(
            "{},\"{}\",{},{}",
            cycle_entry.id().to_string(),
            cycle_entry.display_name().replace('"', "\"\""),
            cycle_entry.start_date(),
            cycle_entry.end_date(),
        );
    }
}

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("—"),
    }
}
