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

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

/// Entry point for the `plane modules` subcommand tree, dispatching to list, create,
/// delete, add, or remove operations on project modules.
/// Usage: `plane modules ls`, `plane modules create "Auth System"`,
///        `plane modules add NRO-123 "Auth System"`
#[derive(clap::Args)]
pub struct ModuleCommandArgs {
    /// The module action to perform
    #[command(subcommand)]
    action: ModuleCommandAction,
}

/// Available module subcommands: listing, creating, deleting, and managing work items
#[derive(clap::Subcommand)]
enum ModuleCommandAction {
    /// List all modules or items in a named module
    Ls(ModuleCommandListArgs),
    /// Create a new module
    Create(ModuleCommandCreateArgs),
    /// Delete a module
    Delete(ModuleCommandDeleteArgs),
    /// Add a work item to a module
    Add(ModuleCommandAddArgs),
    /// Remove a work item from a module
    Remove(ModuleCommandRemoveArgs),
}

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

/// Arguments for listing all modules or work items within a named module.
/// Without a module name: shows all modules with descriptions.
/// With a module name: shows work items assigned to that module.
/// Usage: `plane modules ls` or `plane modules ls "Auth System"`
#[derive(clap::Args)]
struct ModuleCommandListArgs {
    /// Module name to list items from (omit to list all modules)
    module_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 module or modules list in the default browser instead of printing
    #[arg(long)]
    web: Option<String>,
}

/// Execution logic for the `plane modules ls` subcommand
impl ModuleCommandListArgs {
    /// Either lists all modules or lists work items in a specific named module
    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.module_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("/modules/");
                        super::open::launch_browser(&browser_url)?;
                        println!("Opened modules in browser");
                    }
                    Option::Some(ref target_module_name) => {
                        let project_modules =
                            api_client.list_modules(resolved_project.id()).await?;
                        let mut matched_module_id = Option::None;
                        for candidate_module in &project_modules {
                            if candidate_module
                                .display_name()
                                .eq_ignore_ascii_case(target_module_name)
                            {
                                matched_module_id =
                                    Option::Some(candidate_module.id().as_str().to_owned());
                            }
                        }
                        let resolved_module_uuid = match matched_module_id {
                            Option::Some(module_uuid) => module_uuid,
                            Option::None => {
                                let available: Vec<&str> = project_modules
                                    .iter()
                                    .map(|m| m.display_name())
                                    .collect();
                                anyhow::bail!(
                                    "module '{}' not found — available: {}",
                                    target_module_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("/modules/");
                        browser_url.push_str(&resolved_module_uuid);
                        super::open::launch_browser(&browser_url)?;
                        println!("Opened module {} in browser", target_module_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.module_name {
            Option::None => {
                let project_modules = api_client.list_modules(resolved_project.id()).await?;
                match selected_format {
                    OutputFormat::Table => render_modules_table(&project_modules),
                    OutputFormat::Json => render_modules_json(&project_modules),
                    OutputFormat::Csv => render_modules_csv(&project_modules),
                }
            }
            Option::Some(ref target_module_name) => {
                let project_modules = api_client.list_modules(resolved_project.id()).await?;
                let mut matched_module_id = Option::None;
                for candidate_module in &project_modules {
                    if candidate_module.display_name().eq_ignore_ascii_case(target_module_name) {
                        matched_module_id = Option::Some(candidate_module.id());
                    }
                }
                let resolved_module_id = match matched_module_id {
                    Option::Some(module_id) => module_id,
                    Option::None => {
                        let available: Vec<&str> = project_modules
                            .iter()
                            .map(|m| m.display_name())
                            .collect();
                        anyhow::bail!(
                            "module '{}' not found — available: {}",
                            target_module_name,
                            available.join(", ")
                        );
                    }
                };

                let project_client = api_client.project_operations(resolved_project.id());
                let module_client = project_client.module_operations(resolved_module_id);
                let module_work_items = module_client.list_work_items().await?;

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

                let module_command_item_renderer = ModuleCommandItemRenderer {
                    project_states: &project_states,
                    project_identifier_code,
                };

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

        Ok(())
    }
}

/// Arguments for creating a new module in the active project
#[derive(clap::Args)]
struct ModuleCommandCreateArgs {
    /// Display name for the new module (e.g. "Auth System")
    module_display_name: String,
    /// Optional description for the module
    #[arg(long, default_value = "")]
    description: String,
    /// Project slug to target (overrides the configured default)
    #[arg(long)]
    project: Option<String>,
}

/// Execution logic for the `plane modules create` subcommand
impl ModuleCommandCreateArgs {
    /// Creates a new module in the resolved project
    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_module(CreateModuleParams {
                module_name: &self.module_display_name,
                module_description: &self.description,
            })
            .await?;

        println!("Created module '{}'", self.module_display_name);
        Ok(())
    }
}

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

/// Execution logic for the `plane modules delete` subcommand
impl ModuleCommandDeleteArgs {
    /// Resolves the module 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_modules = api_client.list_modules(resolved_project.id()).await?;
        let resolved_module_id =
            resolve_module_command_target(&project_modules, &self.module_display_name)?;

        let project_client = api_client.project_operations(resolved_project.id());
        project_client.delete_module(resolved_module_id).await?;

        println!("Deleted module '{}'", self.module_display_name);
        Ok(())
    }
}

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

/// Execution logic for the `plane modules add` subcommand
impl ModuleCommandAddArgs {
    /// Resolves the work item and module, then adds the item to the module
    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_modules = api_client.list_modules(resolved_project.id()).await?;
        let resolved_module_id =
            resolve_module_command_target(&project_modules, &self.module_name)?;

        let project_client = api_client.project_operations(resolved_project.id());
        let module_client = project_client.module_operations(resolved_module_id);
        module_client.add_work_item(resolved_work_item.id()).await?;

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

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

/// Execution logic for the `plane modules remove` subcommand
impl ModuleCommandRemoveArgs {
    /// Resolves the work item and module, then removes the item from the module
    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_modules = api_client.list_modules(resolved_project.id()).await?;
        let resolved_module_id =
            resolve_module_command_target(&project_modules, &self.module_name)?;

        let project_client = api_client.project_operations(resolved_project.id());
        let module_client = project_client.module_operations(resolved_module_id);
        module_client
            .remove_work_item(resolved_work_item.id())
            .await?;

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

/// Resolves a module by case-insensitive name match against the list of project modules.
/// Returns the module ID on match, or bails with available module names on miss.
fn resolve_module_command_target<'a>(
    project_modules: &'a [crate::api::PlaneModule],
    target_name: &str,
) -> Result<&'a crate::api::ModuleId> {
    let mut matched_module_id = Option::None;
    for candidate_module in project_modules {
        if candidate_module
            .display_name()
            .eq_ignore_ascii_case(target_name)
        {
            matched_module_id = Option::Some(candidate_module.id());
        }
    }
    match matched_module_id {
        Option::Some(module_id) => Ok(module_id),
        Option::None => {
            let available: Vec<&str> = project_modules
                .iter()
                .map(|m| m.display_name())
                .collect();
            anyhow::bail!(
                "module '{}' not found — available: {}",
                target_name,
                available.join(", ")
            );
        }
    }
}

/// Holds project context for rendering work items within a module command
struct ModuleCommandItemRenderer<'a> {
    project_states: &'a [PlaneState],
    project_identifier_code: &'a str,
}

/// Rendering methods for module work item lists
impl<'a> ModuleCommandItemRenderer<'a> {
    fn print_table(&self, work_items: &[&PlaneWorkItem]) {
        if work_items.is_empty() {
            println!("No items in this module.");
            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_modules_table(project_modules: &[crate::api::PlaneModule]) {
    if project_modules.is_empty() {
        println!("No modules found.");
        return;
    }

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

    for module_entry in project_modules {
        let description_cell = match module_entry.module_description() {
            "" => String::from("—"),
            description_text => description_text.to_owned(),
        };

        terminal_table.add_row(vec![
            Cell::new(module_entry.display_name()),
            Cell::new(description_cell),
        ]);
    }

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

fn render_modules_json(project_modules: &[crate::api::PlaneModule]) {
    let json_entries: Vec<serde_json::Value> = project_modules
        .iter()
        .map(|module_entry| {
            let description_field = match module_entry.module_description() {
                "" => serde_json::Value::Null,
                description_text => serde_json::Value::String(description_text.to_owned()),
            };
            serde_json::json!({
                "id": module_entry.id().to_string(),
                "name": module_entry.display_name(),
                "description": description_field,
            })
        })
        .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_modules_csv(project_modules: &[crate::api::PlaneModule]) {
    println!("id,name,description");
    for module_entry in project_modules {
        println!(
            "{},\"{}\",\"{}\"",
            module_entry.id().to_string(),
            module_entry.display_name().replace('"', "\"\""),
            module_entry.module_description().replace('"', "\"\""),
        );
    }
}

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