use anyhow::Result;

use crate::api::{LabelId, MemberId, PlaneApiClient, WorkItemIdentifier};
use crate::config::resolve_plane_session;

/// Batch-updates fields on multiple work items at once, replacing the need
/// for repeated individual API calls. Supports state, priority, due date,
/// assignee, and label changes with --dry-run to preview before executing.
#[derive(clap::Args)]
pub struct BulkStateTransitionArgs {
    /// Work item identifiers to update (e.g. "NRO-1 NRO-2 NRO-3")
    identifiers: Vec<String>,
    /// Target state name to transition all items to
    #[arg(long)]
    state: Option<String>,
    /// New priority level for all items: urgent, high, medium, low, none
    #[arg(long)]
    priority: Option<String>,
    /// Target date (due date) for all items in YYYY-MM-DD format
    #[arg(long)]
    due: Option<String>,
    /// Assignee member UUID or "me" for the current user (repeatable)
    #[arg(long)]
    assignee: Vec<String>,
    /// Label name to set on all items (repeatable, resolved by name)
    #[arg(long)]
    label: Vec<String>,
    /// Preview changes without sending API requests
    #[arg(long)]
    dry_run: Option<String>,
}

/// Tracks the outcome of each individual work item update in a bulk operation
enum BulkItemOutcome {
    /// The work item was successfully updated
    Succeeded(String),
    /// The work item update failed with the given error message
    Failed(String, String),
}

/// Pre-resolved assignee UUIDs ready to inject into every request body
enum ResolvedAssignees {
    /// No --assignee flags were provided
    Unspecified,
    /// One or more assignee UUIDs resolved and ready to apply
    Resolved(Vec<MemberId>),
}

/// Pre-resolved label UUIDs ready to inject into every request body
enum ResolvedLabels {
    /// No --label flags were provided
    Unspecified,
    /// One or more label UUIDs resolved and ready to apply
    Resolved(Vec<LabelId>),
}

/// Parameters for resolving --assignee arguments into member UUIDs
struct ResolveAssigneesParams<'a> {
    /// The API client for looking up "me" to a member UUID
    api_client: &'a PlaneApiClient,
    /// Raw assignee arguments from the command line
    assignee_args: &'a [String],
}

/// Parameters for resolving --label name arguments into label UUIDs
struct ResolveLabelsParams<'a> {
    /// The API client for fetching project labels
    api_client: &'a PlaneApiClient,
    /// Raw label name arguments from the command line
    label_args: &'a [String],
    /// Work item identifiers used to determine which project to fetch labels from
    identifiers: &'a [String],
}

/// Execution logic for the `plane bulk` subcommand
impl BulkStateTransitionArgs {
    /// Validates all target work items exist, applies the requested field changes
    /// to each, and reports per-item success or failure
    pub async fn execute(self) -> Result<()> {
        if self.identifiers.is_empty() {
            anyhow::bail!("no work item identifiers provided — usage: plane bulk NRO-1 NRO-2 --state Done");
        }

        let has_any_field = self.state.is_some()
            || self.priority.is_some()
            || self.due.is_some()
            || !self.assignee.is_empty()
            || !self.label.is_empty();

        if !has_any_field {
            anyhow::bail!(
                "no fields to update — pass --state, --priority, --due, --assignee, or --label"
            );
        }

        let plane_session = resolve_plane_session()?;
        let api_client = PlaneApiClient::try_new(plane_session)?;

        let resolved_assignees = resolve_assignees(ResolveAssigneesParams {
            api_client: &api_client,
            assignee_args: &self.assignee,
        })
        .await?;

        let resolved_labels = resolve_labels(ResolveLabelsParams {
            api_client: &api_client,
            label_args: &self.label,
            identifiers: &self.identifiers,
        })
        .await?;

        let mut outcomes: Vec<BulkItemOutcome> = Vec::with_capacity(self.identifiers.len());

        for identifier_text in &self.identifiers {
            let parsed_identifier: WorkItemIdentifier = match identifier_text.parse() {
                Ok(parsed) => parsed,
                Err(parse_error) => {
                    outcomes.push(BulkItemOutcome::Failed(
                        identifier_text.to_owned(),
                        format!("{}", parse_error),
                    ));
                    continue;
                }
            };

            let fetched_work_item = match api_client
                .find_work_item_by_identifier(&parsed_identifier)
                .await
            {
                Ok(work_item) => work_item,
                Err(fetch_error) => {
                    outcomes.push(BulkItemOutcome::Failed(
                        identifier_text.to_owned(),
                        format!("{}", fetch_error),
                    ));
                    continue;
                }
            };

            let mut request_fields = serde_json::Map::new();

            if let Option::Some(ref priority_text) = self.priority {
                request_fields.insert(
                    String::from("priority"),
                    serde_json::Value::String(priority_text.to_lowercase()),
                );
            }

            if let Option::Some(ref due_date_text) = self.due {
                request_fields.insert(
                    String::from("target_date"),
                    serde_json::Value::String(due_date_text.to_string()),
                );
            }

            match &resolved_assignees {
                ResolvedAssignees::Unspecified => {}
                ResolvedAssignees::Resolved(member_ids) => {
                    let assignee_json_array: Vec<serde_json::Value> = member_ids
                        .iter()
                        .map(|member_id| serde_json::Value::String(member_id.to_string()))
                        .collect();
                    request_fields.insert(
                        String::from("assignees"),
                        serde_json::Value::Array(assignee_json_array),
                    );
                }
            }

            match &resolved_labels {
                ResolvedLabels::Unspecified => {}
                ResolvedLabels::Resolved(label_ids) => {
                    let label_json_array: Vec<serde_json::Value> = label_ids
                        .iter()
                        .map(|label_id| serde_json::Value::String(label_id.to_string()))
                        .collect();
                    request_fields.insert(
                        String::from("labels"),
                        serde_json::Value::Array(label_json_array),
                    );
                }
            }

            if let Option::Some(ref state_name) = self.state {
                let project_states = match api_client
                    .list_states(fetched_work_item.project_id())
                    .await
                {
                    Ok(states) => states,
                    Err(state_error) => {
                        outcomes.push(BulkItemOutcome::Failed(
                            identifier_text.to_owned(),
                            format!("failed to fetch states: {}", state_error),
                        ));
                        continue;
                    }
                };

                let mut matched_state_id = Option::None;
                for candidate_state in &project_states {
                    if candidate_state
                        .display_name()
                        .eq_ignore_ascii_case(state_name)
                    {
                        matched_state_id = Option::Some(candidate_state.id().to_string());
                    }
                }

                match matched_state_id {
                    Option::Some(state_uuid) => {
                        request_fields.insert(
                            String::from("state"),
                            serde_json::Value::String(state_uuid),
                        );
                    }
                    Option::None => {
                        let available: Vec<&str> =
                            project_states.iter().map(|s| s.display_name()).collect();
                        outcomes.push(BulkItemOutcome::Failed(
                            identifier_text.to_owned(),
                            format!(
                                "state '{}' not found — available: {}",
                                state_name,
                                available.join(", ")
                            ),
                        ));
                        continue;
                    }
                }
            }

            let request_body_json = serde_json::Value::Object(request_fields).to_string();

            match self.dry_run {
                Option::Some(_) => {
                    println!("  dry-run  {} — would apply: {}", identifier_text, request_body_json);
                    outcomes.push(BulkItemOutcome::Succeeded(identifier_text.to_owned()));
                }
                Option::None => {
                    let project_client = api_client.project_operations(fetched_work_item.project_id());
                    let item_client = project_client.work_item_operations(fetched_work_item.id());

                    match item_client.update_fields(&request_body_json).await {
                        Ok(_updated_item) => {
                            outcomes.push(BulkItemOutcome::Succeeded(identifier_text.to_owned()));
                        }
                        Err(update_error) => {
                            outcomes.push(BulkItemOutcome::Failed(
                                identifier_text.to_owned(),
                                format!("{}", update_error),
                            ));
                        }
                    }
                }
            }
        }

        let mut succeeded_count: u32 = 0;
        let mut failed_count: u32 = 0;

        for outcome in &outcomes {
            match outcome {
                BulkItemOutcome::Succeeded(identifier_text) => {
                    println!("  ok  {}", identifier_text);
                    succeeded_count += 1;
                }
                BulkItemOutcome::Failed(identifier_text, error_message) => {
                    eprintln!("  FAIL  {} — {}", identifier_text, error_message);
                    failed_count += 1;
                }
            }
        }

        println!(
            "\n{} succeeded, {} failed",
            succeeded_count, failed_count
        );

        if failed_count > 0 {
            anyhow::bail!("{} items failed to update", failed_count);
        }

        Ok(())
    }
}

/// Resolves --assignee arguments into member UUIDs before the per-item loop.
/// "me" is resolved via the current-user API endpoint; other values are treated
/// as literal UUIDs and parsed directly.
async fn resolve_assignees(params: ResolveAssigneesParams<'_>) -> Result<ResolvedAssignees> {
    if params.assignee_args.is_empty() {
        return Ok(ResolvedAssignees::Unspecified);
    }

    let mut member_ids: Vec<MemberId> = Vec::with_capacity(params.assignee_args.len());

    for assignee_text in params.assignee_args {
        match assignee_text.eq_ignore_ascii_case("me") {
            true => {
                let current_user_id = params.api_client.current_user_member_id().await?;
                member_ids.push(current_user_id);
            }
            false => {
                let parsed_id: MemberId = assignee_text
                    .parse()
                    .map_err(|parse_err: anyhow::Error| {
                        anyhow::anyhow!("invalid assignee UUID '{}': {}", assignee_text, parse_err)
                    })?;
                member_ids.push(parsed_id);
            }
        }
    }

    Ok(ResolvedAssignees::Resolved(member_ids))
}

/// Resolves --label name arguments into label UUIDs before the per-item loop.
/// Fetches labels from the project of the first work item identifier to build
/// a name-to-UUID lookup. Label name matching is case-insensitive.
async fn resolve_labels(params: ResolveLabelsParams<'_>) -> Result<ResolvedLabels> {
    if params.label_args.is_empty() {
        return Ok(ResolvedLabels::Unspecified);
    }

    let first_identifier: WorkItemIdentifier = params
        .identifiers
        .first()
        .ok_or_else(|| anyhow::anyhow!("no identifiers to resolve labels against"))?
        .parse()?;

    let project_slug_text = first_identifier.project_identifier().to_lowercase();
    let project_slug = project_slug_text.parse()?;
    let resolved_project = params.api_client.find_project_by_slug(&project_slug).await?;
    let project_labels = params.api_client.list_labels(resolved_project.id()).await?;

    let mut label_ids: Vec<LabelId> = Vec::with_capacity(params.label_args.len());

    for label_name in params.label_args {
        let mut matched_label_id_text = Option::None;
        for candidate_label in &project_labels {
            if candidate_label
                .display_name()
                .eq_ignore_ascii_case(label_name)
            {
                matched_label_id_text = Option::Some(candidate_label.id().to_string());
            }
        }

        match matched_label_id_text {
            Option::Some(label_id_text) => {
                let label_id: LabelId = label_id_text.parse()?;
                label_ids.push(label_id);
            }
            Option::None => {
                let available: Vec<&str> =
                    project_labels.iter().map(|l| l.display_name()).collect();
                anyhow::bail!(
                    "label '{}' not found — available: {}",
                    label_name,
                    available.join(", ")
                );
            }
        }
    }

    Ok(ResolvedLabels::Resolved(label_ids))
}
