use std::fmt;
use std::num::NonZeroU32;
use std::str::FromStr;

/// Checks whether a string matches UUID v4 format: 8-4-4-4-12 hex digits separated by hyphens.
/// Rejects empty strings, wrong lengths, and non-hex characters to catch corrupted API responses.
fn is_valid_uuid_format(candidate: &str) -> bool {
    if candidate.len() != 36 {
        return false;
    }
    candidate.bytes().enumerate().all(|(position, byte)| match position {
        8 | 13 | 18 | 23 => byte == b'-',
        _ => byte.is_ascii_hexdigit(),
    })
}

macro_rules! define_plane_entity_id {
    (
        $(#[$type_docs:meta])*
        $type_name:ident, $entity_label:expr
    ) => {
        $(#[$type_docs])*
        #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
        #[serde(transparent)]
        pub struct $type_name(String);

        /// UUID string access for API URL construction and request payloads
        impl $type_name {
            /// Returns the UUID string for embedding in Plane API endpoint paths
            pub fn as_str(&self) -> &str {
                &self.0
            }
        }

        impl FromStr for $type_name {
            type Err = anyhow::Error;

            fn from_str(raw_value: &str) -> anyhow::Result<Self> {
                if !is_valid_uuid_format(raw_value) {
                    anyhow::bail!(
                        "invalid {} identifier: expected UUID format \
                         (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), got: {}",
                        $entity_label,
                        raw_value
                    );
                }
                Ok(Self(raw_value.to_owned()))
            }
        }

        impl fmt::Display for $type_name {
            fn fmt(&self, uuid_display: &mut fmt::Formatter<'_>) -> fmt::Result {
                uuid_display.write_str(&self.0)
            }
        }

        impl<'de> serde::Deserialize<'de> for $type_name {
            fn deserialize<D: serde::Deserializer<'de>>(
                json_input: D,
            ) -> Result<Self, D::Error> {
                let raw_text = <String as serde::Deserialize>::deserialize(json_input)?;
                raw_text.parse().map_err(serde::de::Error::custom)
            }
        }
    };
}

define_plane_entity_id!(
    /// UUID assigned by Plane to a work item, serving as the primary key in API paths
    /// and the anchor for comments, relations, and activity references
    WorkItemId,
    "work item"
);

define_plane_entity_id!(
    /// UUID assigned by Plane to a project, scoping all work item, state, label,
    /// cycle, and module API operations within a workspace
    ProjectId,
    "project"
);

define_plane_entity_id!(
    /// UUID assigned by Plane to a workflow state within a project, referenced by
    /// work items to indicate their current position in the workflow
    StateId,
    "state"
);

define_plane_entity_id!(
    /// UUID assigned by Plane to a workspace member, used in assignee lists
    /// and activity attribution across work items
    MemberId,
    "member"
);

define_plane_entity_id!(
    /// UUID assigned by Plane to a label within a project, used for categorical
    /// tagging and filtering of work items
    LabelId,
    "label"
);

define_plane_entity_id!(
    /// UUID assigned by Plane to a cycle (time-boxed sprint) within a project,
    /// used to scope work items to a specific iteration
    CycleId,
    "cycle"
);

define_plane_entity_id!(
    /// UUID assigned by Plane to a module (feature group) within a project,
    /// used to organize work items by goal or functional area
    ModuleId,
    "module"
);

define_plane_entity_id!(
    /// UUID assigned by Plane to a workspace, returned in API responses as
    /// the workspace field on projects, states, and work items
    WorkspaceId,
    "workspace"
);

/// Position of a work item within its project's sequential numbering scheme.
/// Combined with the project identifier to form human-readable references like "NRO-123".
/// Plane assigns these starting at 1 and incrementing monotonically per project.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SequenceNumber(NonZeroU32);

/// Numeric access for constructing human-readable work item identifiers
impl SequenceNumber {
    /// Returns the underlying integer for formatting identifiers (e.g. 123 in "NRO-123")
    pub fn as_u32(&self) -> u32 {
        self.0.get()
    }
}

impl FromStr for SequenceNumber {
    type Err = anyhow::Error;

    fn from_str(raw_value: &str) -> anyhow::Result<Self> {
        let parsed: u32 = raw_value.parse().map_err(|parse_error| {
            anyhow::anyhow!(
                "invalid sequence number '{}': {}",
                raw_value,
                parse_error
            )
        })?;
        let non_zero = NonZeroU32::new(parsed)
            .ok_or_else(|| anyhow::anyhow!("sequence number must be greater than zero"))?;
        Ok(Self(non_zero))
    }
}

impl fmt::Display for SequenceNumber {
    fn fmt(&self, sequence_display: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(sequence_display, "{}", self.0)
    }
}

impl serde::Serialize for SequenceNumber {
    fn serialize<S: serde::Serializer>(&self, serde_output: S) -> Result<S::Ok, S::Error> {
        serde_output.serialize_u32(self.0.get())
    }
}

impl<'de> serde::Deserialize<'de> for SequenceNumber {
    fn deserialize<D: serde::Deserializer<'de>>(serde_input: D) -> Result<Self, D::Error> {
        let raw_value = <u32 as serde::Deserialize>::deserialize(serde_input)?;
        let non_zero = NonZeroU32::new(raw_value)
            .ok_or_else(|| serde::de::Error::custom("sequence number must be greater than zero"))?;
        Ok(Self(non_zero))
    }
}

/// Human-readable work item reference combining a project identifier and sequence number.
/// Parsed from user input like "NRO-123" where "NRO" is the project identifier code
/// and "123" is the monotonic sequence number assigned by Plane.
#[derive(Debug, Clone)]
pub struct WorkItemIdentifier {
    project_identifier: String,
    sequence_number: SequenceNumber,
}

/// Field access for the project and sequence components of a parsed work item reference
impl WorkItemIdentifier {
    /// Returns the project identifier portion (e.g. "NRO" from "NRO-123") for matching
    /// against projects when resolving to an API-level project UUID
    pub fn project_identifier(&self) -> &str {
        &self.project_identifier
    }

    /// Returns the sequence number portion (e.g. 123 from "NRO-123") for use in
    /// the retrieve-by-identifier API endpoint
    pub fn sequence_number(&self) -> SequenceNumber {
        self.sequence_number
    }
}

/// Branch name scanning for work item identifier extraction
impl WorkItemIdentifier {
    /// Scans a git branch name for a work item identifier pattern (UPPERCASE-DIGITS).
    /// Extracts the first match from names like "feat/NRO-123-add-auth" -> NRO-123.
    /// Returns None if no recognizable identifier pattern exists in the branch name.
    pub fn try_extract_from_branch(branch_name: &str) -> Option<Self> {
        let name_bytes = branch_name.as_bytes();
        let total_length = name_bytes.len();
        let mut position: usize = 0;

        while position < total_length {
            let current_byte = match name_bytes.get(position) {
                Option::Some(byte_value) => *byte_value,
                Option::None => break,
            };

            if !current_byte.is_ascii_uppercase() {
                position += 1;
                continue;
            }

            let project_start = position;

            loop {
                let scan_byte = match name_bytes.get(position) {
                    Option::Some(byte_value) => *byte_value,
                    Option::None => break,
                };
                if !scan_byte.is_ascii_uppercase() {
                    break;
                }
                position += 1;
            }

            let hyphen_byte = match name_bytes.get(position) {
                Option::Some(byte_value) => *byte_value,
                Option::None => continue,
            };

            if hyphen_byte != b'-' {
                continue;
            }

            position += 1;
            let number_start = position;

            loop {
                let digit_byte = match name_bytes.get(position) {
                    Option::Some(byte_value) => *byte_value,
                    Option::None => break,
                };
                if !digit_byte.is_ascii_digit() {
                    break;
                }
                position += 1;
            }

            if number_start == position {
                continue;
            }

            let candidate_slice = &branch_name[project_start..position];
            if let Ok(parsed_identifier) = candidate_slice.parse::<Self>() {
                return Option::Some(parsed_identifier);
            }
        }

        Option::None
    }
}

impl FromStr for WorkItemIdentifier {
    type Err = anyhow::Error;

    fn from_str(raw_value: &str) -> anyhow::Result<Self> {
        let hyphen_position = raw_value.rfind('-').ok_or_else(|| {
            anyhow::anyhow!(
                "invalid work item identifier '{}': expected format like NRO-123",
                raw_value
            )
        })?;

        let project_part = raw_value.get(..hyphen_position).ok_or_else(|| {
            anyhow::anyhow!(
                "invalid work item identifier '{}': failed to extract project portion",
                raw_value
            )
        })?;

        let number_start = hyphen_position.checked_add(1).ok_or_else(|| {
            anyhow::anyhow!(
                "invalid work item identifier '{}': identifier too long",
                raw_value
            )
        })?;

        let number_part = raw_value.get(number_start..).ok_or_else(|| {
            anyhow::anyhow!(
                "invalid work item identifier '{}': failed to extract sequence number",
                raw_value
            )
        })?;

        if project_part.is_empty() {
            anyhow::bail!(
                "invalid work item identifier '{}': project identifier portion is empty",
                raw_value
            );
        }

        let sequence_number: SequenceNumber = number_part.parse()?;

        Ok(Self {
            project_identifier: project_part.to_uppercase(),
            sequence_number,
        })
    }
}

impl fmt::Display for WorkItemIdentifier {
    fn fmt(&self, identifier_display: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            identifier_display,
            "{}-{}",
            self.project_identifier, self.sequence_number
        )
    }
}
