use std::num::NonZeroU32;

use anyhow::Result;

use crate::api::{PlaneApiClient, WorkItemDescription, WorkItemIdentifier};
use crate::config::resolve_plane_session;
use crate::git_context::{BranchInference, infer_work_item_from_branch};

/// Views acceptance criteria checkbox status, checks off completed items by index,
/// and appends new criteria to a work item — three AC operations in one subcommand.
/// With just an identifier, displays the AC list. With `check <index>`, toggles a checkbox.
/// With `add "text"`, appends a new criterion. Infers the work item from git branch if omitted.
#[derive(clap::Args)]
pub struct AcceptanceCriteriaArgs {
    /// Work item identifier (e.g. "NRO-123"). Inferred from git branch if omitted.
    identifier: Option<String>,
    /// Sub-action: "check <index>" to toggle, "add <text>" to append, omit to view
    action: Option<String>,
    /// Argument for the sub-action (index number or criteria text)
    action_argument: Option<String>,
}

/// The resolved AC operation after parsing the action and action_argument fields
enum ResolvedAcAction {
    /// Display the current acceptance criteria list (no mutation)
    ViewCriteria,
    /// Append a new unchecked criterion with the given text
    AppendCriterion(String),
    /// Toggle the checked state of the criterion at the given 1-based index
    ToggleCriterion(NonZeroU32),
}

/// Action resolution from raw clap fields
impl AcceptanceCriteriaArgs {
    /// Parses the raw action and action_argument strings into a typed AC operation.
    /// Returns ViewCriteria when no action is provided, AppendCriterion for "add",
    /// and ToggleCriterion for "check" with a validated 1-based index.
    fn resolve_action(&self) -> Result<ResolvedAcAction> {
        match self.action {
            Option::None => Ok(ResolvedAcAction::ViewCriteria),
            Option::Some(ref action_text) => match action_text.as_str() {
                "add" => match self.action_argument {
                    Option::Some(ref criterion_text) => {
                        Ok(ResolvedAcAction::AppendCriterion(criterion_text.to_owned()))
                    }
                    Option::None => {
                        anyhow::bail!(
                            "'add' requires criterion text — \
                             usage: plane ac NRO-123 add \"text\""
                        )
                    }
                },
                "check" => match self.action_argument {
                    Option::Some(ref index_text) => {
                        let parsed_index: u32 =
                            index_text.parse().map_err(|parse_error| {
                                anyhow::anyhow!(
                                    "invalid criterion index '{}': {} — \
                                     usage: plane ac NRO-123 check 1",
                                    index_text,
                                    parse_error
                                )
                            })?;
                        let validated_index =
                            NonZeroU32::new(parsed_index).ok_or_else(|| {
                                anyhow::anyhow!("criterion index must be >= 1")
                            })?;
                        Ok(ResolvedAcAction::ToggleCriterion(validated_index))
                    }
                    Option::None => {
                        anyhow::bail!(
                            "'check' requires a criterion index — \
                             usage: plane ac NRO-123 check 1"
                        )
                    }
                },
                unknown_action => {
                    anyhow::bail!(
                        "unknown AC action '{}' — valid actions: add, check",
                        unknown_action
                    )
                }
            },
        }
    }
}

/// Execution logic for the `plane ac` subcommand
impl AcceptanceCriteriaArgs {
    /// Dispatches to the appropriate AC operation (view, check, or add)
    /// based on the provided sub-arguments
    pub async fn execute(self) -> Result<()> {
        let plane_session = resolve_plane_session()?;
        let api_client = PlaneApiClient::try_new(plane_session)?;

        let work_item_identifier: WorkItemIdentifier = match self.identifier {
            Option::Some(ref identifier_text) => identifier_text.parse()?,
            Option::None => match infer_work_item_from_branch() {
                BranchInference::Resolved(inferred_identifier) => inferred_identifier,
                BranchInference::NotInGitRepo => {
                    anyhow::bail!(
                        "no identifier provided and not in a git repo — \
                         usage: plane ac NRO-123"
                    );
                }
                BranchInference::NoIdentifierInBranch(branch_name) => {
                    anyhow::bail!(
                        "no identifier provided and branch '{}' \
                         does not contain a work item ID",
                        branch_name
                    );
                }
            },
        };

        let resolved_action = self.resolve_action()?;

        let fetched_work_item = api_client
            .find_work_item_by_identifier(&work_item_identifier)
            .await?;

        match resolved_action {
            ResolvedAcAction::ViewCriteria => {
                let criteria_printer = CriteriaListPrinter {
                    work_item_identifier: &work_item_identifier,
                };
                criteria_printer.print(fetched_work_item.description_stripped());
            }
            ResolvedAcAction::AppendCriterion(criterion_text) => {
                let task_list_editor =
                    TaskListEditor::from_description(fetched_work_item.description_html());
                let updated_html = task_list_editor.append_criterion(&criterion_text);

                let mut request_fields = serde_json::Map::new();
                request_fields.insert(
                    String::from("description_html"),
                    serde_json::Value::String(updated_html),
                );
                let request_body_json =
                    serde_json::Value::Object(request_fields).to_string();

                let project_client =
                    api_client.project_operations(fetched_work_item.project_id());
                let item_client =
                    project_client.work_item_operations(fetched_work_item.id());
                item_client.update_fields(&request_body_json).await?;

                println!(
                    "Added criterion to {}: {}",
                    work_item_identifier, criterion_text
                );
            }
            ResolvedAcAction::ToggleCriterion(target_index) => {
                let task_list_editor =
                    TaskListEditor::from_description(fetched_work_item.description_html());
                let updated_html = task_list_editor.toggle_criterion(target_index)?;

                let mut request_fields = serde_json::Map::new();
                request_fields.insert(
                    String::from("description_html"),
                    serde_json::Value::String(updated_html),
                );
                let request_body_json =
                    serde_json::Value::Object(request_fields).to_string();

                let project_client =
                    api_client.project_operations(fetched_work_item.project_id());
                let item_client =
                    project_client.work_item_operations(fetched_work_item.id());
                item_client.update_fields(&request_body_json).await?;

                println!(
                    "Toggled criterion {} on {}",
                    target_index, work_item_identifier
                );
            }
        }

        Ok(())
    }
}

/// Renders the numbered acceptance criteria list to the terminal, extracting
/// checkbox lines from the work item's plain-text description
struct CriteriaListPrinter<'a> {
    work_item_identifier: &'a WorkItemIdentifier,
}

/// Criteria list display with 1-based numbering matching the check command
impl<'a> CriteriaListPrinter<'a> {
    /// Prints each checkbox line from the description with a 1-based index
    /// that corresponds to the `check <index>` argument
    fn print(&self, description: &WorkItemDescription) {
        match description {
            WorkItemDescription::Present(description_text) => {
                println!(
                    "Acceptance Criteria for {}:",
                    self.work_item_identifier
                );
                println!();
                let mut criterion_index: u32 = 0;
                for line in description_text.lines() {
                    let trimmed_line = line.trim();
                    if trimmed_line.starts_with("- [")
                        || trimmed_line.starts_with("* [")
                    {
                        criterion_index += 1;
                        println!("  {}. {}", criterion_index, trimmed_line);
                    }
                }
                if criterion_index == 0 {
                    println!("  (no acceptance criteria found)");
                }
            }
            WorkItemDescription::Absent => {
                println!(
                    "{}: no description or acceptance criteria found",
                    self.work_item_identifier
                );
            }
        }
    }
}

/// Holds the current HTML description content for task list manipulation.
/// Provides consuming methods that produce the updated HTML string for PATCHing
/// back to the Plane API via the description_html field.
struct TaskListEditor {
    html_content: String,
}

/// Construction from a work item's description field
impl TaskListEditor {
    /// Extracts the HTML content from a description enum, defaulting to empty
    /// for absent descriptions (a new taskList will be created on append)
    fn from_description(description: &WorkItemDescription) -> Self {
        match description {
            WorkItemDescription::Present(html_text) => Self {
                html_content: html_text.to_owned(),
            },
            WorkItemDescription::Absent => Self {
                html_content: String::new(),
            },
        }
    }
}

/// Task list mutation operations that consume the editor and return updated HTML
impl TaskListEditor {
    /// Appends a new unchecked task item to the description's taskList.
    /// If no taskList exists, creates one. If the description already has a
    /// taskList, inserts the new item before the closing `</ul>` tag.
    fn append_criterion(self, criterion_text: &str) -> String {
        let new_item = format!(
            "<li data-type=\"taskItem\" data-checked=\"false\"><p>{}</p></li>",
            criterion_text
        );

        if self.html_content.contains("data-type=\"taskList\"") {
            match self.html_content.rfind("</ul>") {
                Option::Some(insert_position) => {
                    let before_close =
                        match self.html_content.get(..insert_position) {
                            Option::Some(text_slice) => text_slice,
                            Option::None => return self.html_content,
                        };
                    let after_close =
                        match self.html_content.get(insert_position..) {
                            Option::Some(text_slice) => text_slice,
                            Option::None => return self.html_content,
                        };
                    let mut result = String::with_capacity(
                        before_close.len() + new_item.len() + after_close.len(),
                    );
                    result.push_str(before_close);
                    result.push_str(&new_item);
                    result.push_str(after_close);
                    result
                }
                Option::None => {
                    let mut result = self.html_content;
                    result.push_str("<ul data-type=\"taskList\">");
                    result.push_str(&new_item);
                    result.push_str("</ul>");
                    result
                }
            }
        } else {
            let mut result = self.html_content;
            result.push_str("<ul data-type=\"taskList\">");
            result.push_str(&new_item);
            result.push_str("</ul>");
            result
        }
    }

    /// Toggles the data-checked attribute of the nth task item (1-based index).
    /// Searches the HTML for `data-type="taskItem"` occurrences, counts to the
    /// target index, then flips `data-checked="false"` to `"true"` or vice versa.
    fn toggle_criterion(self, target_index: NonZeroU32) -> Result<String> {
        let task_item_marker = "data-type=\"taskItem\"";
        let checked_false_attr = "data-checked=\"false\"";
        let checked_true_attr = "data-checked=\"true\"";

        let target_ordinal = target_index.get();
        let mut current_ordinal: u32 = 0;
        let mut search_offset: usize = 0;

        loop {
            let remaining_text = match self.html_content.get(search_offset..) {
                Option::Some(text_slice) => text_slice,
                Option::None => {
                    anyhow::bail!(
                        "criterion {} not found — this item has {} criteria",
                        target_ordinal,
                        current_ordinal
                    );
                }
            };

            let marker_relative_pos = match remaining_text.find(task_item_marker) {
                Option::Some(found_position) => found_position,
                Option::None => {
                    anyhow::bail!(
                        "criterion {} not found — this item has {} criteria",
                        target_ordinal,
                        current_ordinal
                    );
                }
            };

            current_ordinal += 1;
            let marker_absolute_pos = search_offset + marker_relative_pos;

            if current_ordinal == target_ordinal {
                let search_window_start =
                    match marker_absolute_pos.checked_sub(80) {
                        Option::Some(earlier_pos) => earlier_pos,
                        Option::None => 0,
                    };
                let search_window_end = std::cmp::min(
                    marker_absolute_pos + 200,
                    self.html_content.len(),
                );
                let search_window = match self
                    .html_content
                    .get(search_window_start..search_window_end)
                {
                    Option::Some(window_text) => window_text,
                    Option::None => {
                        anyhow::bail!(
                            "failed to extract search window for criterion {}",
                            target_ordinal
                        );
                    }
                };

                if let Option::Some(attr_relative_pos) =
                    search_window.find(checked_false_attr)
                {
                    let attr_absolute_pos =
                        search_window_start + attr_relative_pos;
                    let before_attr =
                        match self.html_content.get(..attr_absolute_pos) {
                            Option::Some(s) => s,
                            Option::None => {
                                anyhow::bail!("invalid position in HTML")
                            }
                        };
                    let after_start =
                        attr_absolute_pos + checked_false_attr.len();
                    let after_attr =
                        match self.html_content.get(after_start..) {
                            Option::Some(s) => s,
                            Option::None => {
                                anyhow::bail!("invalid position in HTML")
                            }
                        };
                    let mut toggled = String::with_capacity(
                        before_attr.len()
                            + checked_true_attr.len()
                            + after_attr.len(),
                    );
                    toggled.push_str(before_attr);
                    toggled.push_str(checked_true_attr);
                    toggled.push_str(after_attr);
                    return Ok(toggled);
                }

                if let Option::Some(attr_relative_pos) =
                    search_window.find(checked_true_attr)
                {
                    let attr_absolute_pos =
                        search_window_start + attr_relative_pos;
                    let before_attr =
                        match self.html_content.get(..attr_absolute_pos) {
                            Option::Some(s) => s,
                            Option::None => {
                                anyhow::bail!("invalid position in HTML")
                            }
                        };
                    let after_start =
                        attr_absolute_pos + checked_true_attr.len();
                    let after_attr =
                        match self.html_content.get(after_start..) {
                            Option::Some(s) => s,
                            Option::None => {
                                anyhow::bail!("invalid position in HTML")
                            }
                        };
                    let mut toggled = String::with_capacity(
                        before_attr.len()
                            + checked_false_attr.len()
                            + after_attr.len(),
                    );
                    toggled.push_str(before_attr);
                    toggled.push_str(checked_false_attr);
                    toggled.push_str(after_attr);
                    return Ok(toggled);
                }

                anyhow::bail!(
                    "criterion {} has no data-checked attribute — \
                     HTML may be malformed",
                    target_ordinal
                );
            }

            search_offset = marker_absolute_pos + task_item_marker.len();
        }
    }
}
