use anyhow::Result;

use crate::api::{PlaneApiClient, ProjectId, WorkItemIdentifier};

/// Whether a string-valued field flag was provided on the command line.
/// Used for flags where absence means "do not change" and presence means
/// "set to this value". The `Unset` variant avoids Option in the domain layer.
#[derive(Debug)]
pub enum WorkItemFieldArgsString {
    /// The flag was not provided — do not modify this field
    Unset,
    /// The flag was provided with the given value
    Specified(String),
}

/// Whether an integer-valued field flag was provided on the command line.
/// Used for --estimate where absence means "do not change" and presence means
/// "set to this value". The `Unset` variant avoids Option in the domain layer.
#[derive(Debug)]
pub enum WorkItemFieldArgsInt {
    /// The flag was not provided — do not modify this field
    Unset,
    /// The flag was provided with the given value
    Specified(i64),
}

/// Construction parameters for `WorkItemFieldArgsTarget`. Groups the mutable request
/// map, API client reference, and project scope needed for name-to-UUID resolution.
pub struct WorkItemFieldArgsTargetParams<'a> {
    /// The JSON request body being built — resolved fields are inserted here
    pub request_fields: &'a mut serde_json::Map<String, serde_json::Value>,
    /// Authenticated API client for resolving names to UUIDs
    pub api_client: &'a PlaneApiClient,
    /// Project UUID scope for label, module, and member resolution
    pub project_id: &'a ProjectId,
}

/// Target for applying resolved work item field values to an API request body.
/// Holds mutable access to the JSON map being constructed and read-only access
/// to the API client and project scope needed for name-to-UUID resolution.
pub struct WorkItemFieldArgsTarget<'a> {
    request_fields: &'a mut serde_json::Map<String, serde_json::Value>,
    api_client: &'a PlaneApiClient,
    project_id: &'a ProjectId,
}

/// Construction for the apply-fields target from grouped parameters
impl<'a> WorkItemFieldArgsTarget<'a> {
    /// Binds the mutable request map, API client, and project scope into a target
    /// that `WorkItemFieldArgs::apply_to` uses for name resolution and field insertion
    pub fn new(params: WorkItemFieldArgsTargetParams<'a>) -> Self {
        Self {
            request_fields: params.request_fields,
            api_client: params.api_client,
            project_id: params.project_id,
        }
    }
}

/// Raw clap arguments collected before conversion into typed `WorkItemFieldArgs`.
/// Groups all the Vec<String> parameters that clap produces for the shared field flags.
pub struct WorkItemFieldArgsRaw {
    /// Raw --label values (repeatable)
    pub labels: Vec<String>,
    /// Raw --module value (0 or 1 element)
    pub module: Vec<String>,
    /// Raw --parent value (0 or 1 element)
    pub parent: Vec<String>,
    /// Raw --start-date value (0 or 1 element)
    pub start_date: Vec<String>,
    /// Raw --due value (0 or 1 element)
    pub due: Vec<String>,
    /// Raw --description value (0 or 1 element)
    pub description: Vec<String>,
    /// Raw --estimate value (0 or 1 element)
    pub estimate: Vec<String>,
    /// Raw --assignee values (repeatable)
    pub assignees: Vec<String>,
}

/// Collected field flags shared between `plane create` and `plane update`.
/// Each field uses a domain-specific enum to distinguish "not provided" from "provided".
/// Constructed via `from_raw_args` and applied via `apply_to`.
pub struct WorkItemFieldArgs {
    /// Label names to resolve to UUIDs and attach (repeatable --label)
    labels: Vec<String>,
    /// Module name to resolve to UUID
    module_name: WorkItemFieldArgsString,
    /// Parent work item identifier (e.g. "NRO-45") to resolve to UUID
    parent_identifier: WorkItemFieldArgsString,
    /// Start date in YYYY-MM-DD format
    start_date: WorkItemFieldArgsString,
    /// Due/target date in YYYY-MM-DD format
    due_date: WorkItemFieldArgsString,
    /// Plain text description, wrapped in <p> tags before sending
    description: WorkItemFieldArgsString,
    /// Estimate points 0-7
    estimate: WorkItemFieldArgsInt,
    /// Assignee display names to resolve to member UUIDs (repeatable --assignee)
    assignees: Vec<String>,
}

/// Construction from raw clap arguments into typed field args
impl WorkItemFieldArgs {
    /// Converts the raw clap argument vectors into a typed `WorkItemFieldArgs`.
    /// Each Vec<String> with 0 or 1 elements maps to the corresponding enum variant.
    /// Vecs with multiple elements are kept as-is for repeatable flags (labels, assignees).
    pub fn from_raw_args(raw: WorkItemFieldArgsRaw) -> Result<Self> {
        let mut raw_module = raw.module;
        let mut raw_parent = raw.parent;
        let mut raw_start_date = raw.start_date;
        let mut raw_due = raw.due;
        let mut raw_description = raw.description;
        let mut raw_estimate = raw.estimate;

        let module_name = match raw_module.pop() {
            Option::Some(value) => WorkItemFieldArgsString::Specified(value),
            Option::None => WorkItemFieldArgsString::Unset,
        };
        let parent_identifier = match raw_parent.pop() {
            Option::Some(value) => WorkItemFieldArgsString::Specified(value),
            Option::None => WorkItemFieldArgsString::Unset,
        };
        let start_date = match raw_start_date.pop() {
            Option::Some(value) => WorkItemFieldArgsString::Specified(value),
            Option::None => WorkItemFieldArgsString::Unset,
        };
        let due_date = match raw_due.pop() {
            Option::Some(value) => WorkItemFieldArgsString::Specified(value),
            Option::None => WorkItemFieldArgsString::Unset,
        };
        let description = match raw_description.pop() {
            Option::Some(value) => WorkItemFieldArgsString::Specified(value),
            Option::None => WorkItemFieldArgsString::Unset,
        };
        let estimate = match raw_estimate.pop() {
            Option::Some(value) => {
                let parsed_value: i64 = value
                    .parse()
                    .map_err(|e| anyhow::anyhow!("invalid estimate '{}': {}", value, e))?;
                WorkItemFieldArgsInt::Specified(parsed_value)
            }
            Option::None => WorkItemFieldArgsInt::Unset,
        };

        Ok(Self {
            labels: raw.labels,
            module_name,
            parent_identifier,
            start_date,
            due_date,
            description,
            estimate,
            assignees: raw.assignees,
        })
    }
}

/// Validates a date string matches YYYY-MM-DD format with valid month/day ranges.
/// Returns an error on failure, or Ok(()) on success.
fn validate_date_format(date_text: &str) -> Result<()> {
    if date_text.len() != 10 {
        anyhow::bail!(
            "invalid date '{}' — expected YYYY-MM-DD format",
            date_text
        );
    }
    let date_bytes = date_text.as_bytes();
    let fourth_byte = match date_bytes.get(4) {
        Option::Some(byte_value) => *byte_value,
        Option::None => {
            anyhow::bail!("invalid date '{}' — expected YYYY-MM-DD format", date_text);
        }
    };
    let seventh_byte = match date_bytes.get(7) {
        Option::Some(byte_value) => *byte_value,
        Option::None => {
            anyhow::bail!("invalid date '{}' — expected YYYY-MM-DD format", date_text);
        }
    };
    if fourth_byte != b'-' || seventh_byte != b'-' {
        anyhow::bail!(
            "invalid date '{}' — expected YYYY-MM-DD format",
            date_text
        );
    }
    let year_digits = date_text[..4].chars().all(|c| c.is_ascii_digit());
    let month_digits = date_text[5..7].chars().all(|c| c.is_ascii_digit());
    let day_digits = date_text[8..10].chars().all(|c| c.is_ascii_digit());
    if !year_digits || !month_digits || !day_digits {
        anyhow::bail!(
            "invalid date '{}' — expected YYYY-MM-DD format",
            date_text
        );
    }
    let month_value: u32 = date_text[5..7]
        .parse()
        .map_err(|e| anyhow::anyhow!("invalid month in date '{}': {}", date_text, e))?;
    let day_value: u32 = date_text[8..10]
        .parse()
        .map_err(|e| anyhow::anyhow!("invalid day in date '{}': {}", date_text, e))?;
    if month_value < 1 || month_value > 12 {
        anyhow::bail!(
            "invalid date '{}' — month must be 01-12",
            date_text
        );
    }
    if day_value < 1 || day_value > 31 {
        anyhow::bail!(
            "invalid date '{}' — day must be 01-31",
            date_text
        );
    }
    Ok(())
}

/// Field resolution and insertion into the API request body
impl WorkItemFieldArgs {
    /// Validates all provided fields, resolves names to UUIDs via the API,
    /// and inserts the resolved values into the target request body map.
    /// Skips fields that were not provided (Unset variants).
    pub async fn apply_to(&self, target: WorkItemFieldArgsTarget<'_>) -> Result<()> {
        // Labels: resolve each name to a UUID
        if !self.labels.is_empty() {
            let project_labels = target.api_client.list_labels(target.project_id).await?;
            let mut resolved_label_ids: Vec<serde_json::Value> =
                Vec::with_capacity(self.labels.len());

            for label_name in &self.labels {
                let mut matched_label_id = Option::None;
                for candidate_label in &project_labels {
                    if candidate_label
                        .display_name()
                        .eq_ignore_ascii_case(label_name)
                    {
                        matched_label_id =
                            Option::Some(candidate_label.id().as_str().to_owned());
                    }
                }
                match matched_label_id {
                    Option::Some(label_uuid) => {
                        resolved_label_ids.push(serde_json::Value::String(label_uuid));
                    }
                    Option::None => {
                        let available: Vec<&str> = project_labels
                            .iter()
                            .map(|label| label.display_name())
                            .collect();
                        anyhow::bail!(
                            "label '{}' not found — available: {}",
                            label_name,
                            available.join(", ")
                        );
                    }
                }
            }

            target.request_fields.insert(
                String::from("labels"),
                serde_json::Value::Array(resolved_label_ids),
            );
        }

        // Module: resolve name to UUID
        match &self.module_name {
            WorkItemFieldArgsString::Unset => {}
            WorkItemFieldArgsString::Specified(module_name_text) => {
                let project_modules =
                    target.api_client.list_modules(target.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(module_name_text)
                    {
                        matched_module_id =
                            Option::Some(candidate_module.id().as_str().to_owned());
                    }
                }
                match matched_module_id {
                    Option::Some(module_uuid) => {
                        target.request_fields.insert(
                            String::from("module"),
                            serde_json::Value::String(module_uuid),
                        );
                    }
                    Option::None => {
                        let available: Vec<&str> = project_modules
                            .iter()
                            .map(|m| m.display_name())
                            .collect();
                        anyhow::bail!(
                            "module '{}' not found — available: {}",
                            module_name_text,
                            available.join(", ")
                        );
                    }
                }
            }
        }

        // Parent: resolve identifier to UUID
        match &self.parent_identifier {
            WorkItemFieldArgsString::Unset => {}
            WorkItemFieldArgsString::Specified(parent_id_text) => {
                let parsed_identifier: WorkItemIdentifier = parent_id_text.parse()?;
                let parent_work_item = target
                    .api_client
                    .find_work_item_by_identifier(&parsed_identifier)
                    .await?;
                target.request_fields.insert(
                    String::from("parent"),
                    serde_json::Value::String(
                        parent_work_item.id().as_str().to_owned(),
                    ),
                );
            }
        }

        // Start date: validate YYYY-MM-DD
        match &self.start_date {
            WorkItemFieldArgsString::Unset => {}
            WorkItemFieldArgsString::Specified(date_text) => {
                validate_date_format(date_text)?;
                target.request_fields.insert(
                    String::from("start_date"),
                    serde_json::Value::String(date_text.to_owned()),
                );
            }
        }

        // Due date: validate YYYY-MM-DD
        match &self.due_date {
            WorkItemFieldArgsString::Unset => {}
            WorkItemFieldArgsString::Specified(date_text) => {
                validate_date_format(date_text)?;
                target.request_fields.insert(
                    String::from("target_date"),
                    serde_json::Value::String(date_text.to_owned()),
                );
            }
        }

        // Description: wrap in <p> tags
        match &self.description {
            WorkItemFieldArgsString::Unset => {}
            WorkItemFieldArgsString::Specified(description_text) => {
                let html_wrapped = format!("<p>{}</p>", description_text);
                target.request_fields.insert(
                    String::from("description_html"),
                    serde_json::Value::String(html_wrapped),
                );
            }
        }

        // Estimate: validate 0-7
        match &self.estimate {
            WorkItemFieldArgsInt::Unset => {}
            WorkItemFieldArgsInt::Specified(estimate_value) => {
                if *estimate_value < 0 || *estimate_value > 7 {
                    anyhow::bail!(
                        "estimate must be between 0 and 7, got: {}",
                        estimate_value
                    );
                }
                target.request_fields.insert(
                    String::from("estimate_point"),
                    serde_json::Value::Number(
                        serde_json::Number::from(*estimate_value),
                    ),
                );
            }
        }

        // Assignees: pass through as UUIDs (workspace member resolution not yet implemented)
        if !self.assignees.is_empty() {
            let mut assignee_uuids: Vec<serde_json::Value> =
                Vec::with_capacity(self.assignees.len());
            for assignee_value in &self.assignees {
                assignee_uuids.push(serde_json::Value::String(assignee_value.to_owned()));
            }
            target.request_fields.insert(
                String::from("assignees"),
                serde_json::Value::Array(assignee_uuids),
            );
        }

        Ok(())
    }
}
