use std::fmt;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DimensionProfile {
    TenantScoped,
    PermissionScoped,
    TenantPermissionScoped,
    TenantPermissionSearch,
    PagedSearch,
    CursorList,
    LocaleRegionScoped,
    FeatureFlagScoped,
    Custom(CustomProfile),
}

impl DimensionProfile {
    pub fn name(&self) -> &str {
        match self {
            Self::TenantScoped => "tenant_scoped",
            Self::PermissionScoped => "permission_scoped",
            Self::TenantPermissionScoped => "tenant_permission_scoped",
            Self::TenantPermissionSearch => "tenant_permission_search",
            Self::PagedSearch => "paged_search",
            Self::CursorList => "cursor_list",
            Self::LocaleRegionScoped => "locale_region_scoped",
            Self::FeatureFlagScoped => "feature_flag_scoped",
            Self::Custom(profile) => profile.name(),
        }
    }

    pub fn requirements(&self) -> Vec<DimensionRequirement> {
        match self {
            Self::TenantScoped => vec![DimensionRequirement::linked("tenant")],
            Self::PermissionScoped => vec![DimensionRequirement::linked("permission")],
            Self::TenantPermissionScoped => vec![
                DimensionRequirement::linked("tenant"),
                DimensionRequirement::linked("permission"),
            ],
            Self::TenantPermissionSearch => vec![
                DimensionRequirement::linked("tenant"),
                DimensionRequirement::linked("permission"),
                DimensionRequirement::linked("q"),
                DimensionRequirement::linked("page"),
                DimensionRequirement::linked("sort"),
            ],
            Self::PagedSearch => vec![
                DimensionRequirement::linked("q"),
                DimensionRequirement::linked("page"),
                DimensionRequirement::linked("sort"),
            ],
            Self::CursorList => vec![DimensionRequirement::linked("cursor")],
            Self::LocaleRegionScoped => vec![
                DimensionRequirement::linked("locale"),
                DimensionRequirement::linked("region"),
            ],
            Self::FeatureFlagScoped => vec![DimensionRequirement::linked("feature")],
            Self::Custom(profile) => profile.requirements().to_vec(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomProfile {
    name: String,
    requirements: Vec<DimensionRequirement>,
}

impl CustomProfile {
    pub fn new(
        name: impl Into<String>,
        requirements: impl IntoIterator<Item = DimensionRequirement>,
    ) -> Self {
        Self {
            name: name.into(),
            requirements: requirements.into_iter().collect(),
        }
    }

    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn requirements(&self) -> &[DimensionRequirement] {
        &self.requirements
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DimensionRequirement {
    label: String,
    require_key_tag_link: bool,
}

impl DimensionRequirement {
    pub fn linked(label: impl Into<String>) -> Self {
        Self {
            label: label.into(),
            require_key_tag_link: true,
        }
    }

    pub fn key_only(label: impl Into<String>) -> Self {
        Self {
            label: label.into(),
            require_key_tag_link: false,
        }
    }

    pub fn label(&self) -> &str {
        &self.label
    }

    pub fn require_key_tag_link(&self) -> bool {
        self.require_key_tag_link
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum DimensionValidationMode {
    #[default]
    Warn,
    Deny,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProfileValidation {
    Pass,
    MissingDimensions(Vec<String>),
    UnlinkedDimensions(Vec<String>),
    Allowed {
        status: Box<ProfileValidation>,
        reason: String,
    },
}

impl ProfileValidation {
    pub fn is_pass(&self) -> bool {
        matches!(self, Self::Pass | Self::Allowed { .. })
    }
}

impl fmt::Display for ProfileValidation {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Pass => formatter.write_str("pass"),
            Self::MissingDimensions(labels) => {
                write!(formatter, "missing dimensions: {}", labels.join(", "))
            }
            Self::UnlinkedDimensions(labels) => {
                write!(formatter, "unlinked dimensions: {}", labels.join(", "))
            }
            Self::Allowed { status, reason } => {
                write!(formatter, "allowed {status} because {reason}")
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DimensionAllow {
    label: String,
    reason: String,
}

impl DimensionAllow {
    pub fn new(
        label: impl Into<String>,
        reason: impl Into<String>,
    ) -> Result<Self, DimensionAllowError> {
        let reason = reason.into();
        if reason.trim().is_empty() {
            return Err(DimensionAllowError::EmptyReason);
        }
        Ok(Self {
            label: label.into(),
            reason,
        })
    }

    pub fn label(&self) -> &str {
        &self.label
    }

    pub fn reason(&self) -> &str {
        &self.reason
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DimensionAllowError {
    EmptyReason,
}

impl fmt::Display for DimensionAllowError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyReason => formatter.write_str("dimension allow reason cannot be empty"),
        }
    }
}

impl std::error::Error for DimensionAllowError {}
