//! Git hooks adapter requirement.

use core::any::Any;
use core::fmt;

use shackles_core::{AdapterRequirement, FileSlot, FileSlotError};

use crate::types::ADAPTER_ID;
use crate::{ItemRequirements, ScalarAssertion, TextFileContents, TextFileValueError};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitHooksAdapterRequirement {
    hook: HookKind,
    product_id: ProductId,
    main_contents: ItemRequirements<TextFileContents>,
    fragment_contents: ScalarAssertion<TextFileContents>,
}

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

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProductId(String);

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GitHookFileSlot {
    Main {
        hook: HookKind,
    },
    Fragment {
        hook: HookKind,
        product_id: ProductId,
    },
}

#[derive(Debug, Clone, Copy, Default)]
pub struct GitHookLayout;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GitHookValueError {
    EmptyProductId,
    AbsoluteProductId { value: String },
    PathSeparatorProductId { value: String },
    ParentDirectoryProductId { value: String },
    InvalidContents { field: &'static str },
    ForbiddenMainContents,
    ClosedMainContents,
    FragmentContentsMustEqual,
}

impl AdapterRequirement for GitHooksAdapterRequirement {
    fn adapter_id(&self) -> &'static str {
        ADAPTER_ID
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

impl ProductId {
    pub fn new(value: impl Into<String>) -> Result<Self, GitHookValueError> {
        let value = value.into();
        if value.is_empty() {
            return Err(GitHookValueError::EmptyProductId);
        }
        if value.starts_with('/') || value.starts_with('\\') {
            return Err(GitHookValueError::AbsoluteProductId { value });
        }
        if value.contains('/') || value.contains('\\') {
            return Err(GitHookValueError::PathSeparatorProductId { value });
        }
        if value == "." || value == ".." {
            return Err(GitHookValueError::ParentDirectoryProductId { value });
        }
        Ok(Self(value))
    }

    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl GitHooksAdapterRequirement {
    pub fn new(
        hook: HookKind,
        product_id: ProductId,
        main_contents: ItemRequirements<TextFileContents>,
        fragment_contents: ScalarAssertion<TextFileContents>,
    ) -> Result<Self, GitHookValueError> {
        if !main_contents.forbidden.is_empty() {
            return Err(GitHookValueError::ForbiddenMainContents);
        }
        if main_contents.closed.is_some() {
            return Err(GitHookValueError::ClosedMainContents);
        }
        if !matches!(fragment_contents, ScalarAssertion::Equals(..)) {
            return Err(GitHookValueError::FragmentContentsMustEqual);
        }
        Ok(Self {
            hook,
            product_id,
            main_contents,
            fragment_contents,
        })
    }

    #[must_use]
    pub const fn hook(&self) -> HookKind {
        self.hook
    }

    #[must_use]
    pub const fn product_id(&self) -> &ProductId {
        &self.product_id
    }

    #[must_use]
    pub const fn main_contents(&self) -> &ItemRequirements<TextFileContents> {
        &self.main_contents
    }

    #[must_use]
    pub const fn fragment_contents(&self) -> &ScalarAssertion<TextFileContents> {
        &self.fragment_contents
    }

    pub(crate) fn into_parts(
        self,
    ) -> (
        HookKind,
        ProductId,
        ItemRequirements<TextFileContents>,
        ScalarAssertion<TextFileContents>,
    ) {
        (
            self.hook,
            self.product_id,
            self.main_contents,
            self.fragment_contents,
        )
    }
}

impl GitHookFileSlot {
    #[must_use]
    pub const fn main(hook: HookKind) -> Self {
        Self::Main { hook }
    }

    #[must_use]
    pub fn fragment(hook: HookKind, product_id: ProductId) -> Self {
        Self::Fragment { hook, product_id }
    }

    pub fn into_file_slot(self) -> Result<FileSlot, FileSlotError> {
        match self {
            Self::Main {
                hook: HookKind::PreCommit,
            } => FileSlot::new("git-hook-pre-commit"),
            Self::Fragment {
                hook: HookKind::PreCommit,
                product_id,
            } => FileSlot::new(format!(
                "git-hook-pre-commit-fragment-{}",
                product_id.as_str()
            )),
        }
    }
}

impl GitHookLayout {
    #[must_use]
    pub const fn main_relative_path(hook: HookKind) -> &'static str {
        match hook {
            HookKind::PreCommit => ".githooks/pre-commit",
        }
    }

    #[must_use]
    pub fn fragment_relative_path(hook: HookKind, product_id: &ProductId) -> String {
        match hook {
            HookKind::PreCommit => format!(".githooks/pre-commit.d/{}", product_id.as_str()),
        }
    }
}

impl From<TextFileValueError> for GitHookValueError {
    fn from(value: TextFileValueError) -> Self {
        match value {
            TextFileValueError::Empty { field } => Self::InvalidContents { field },
        }
    }
}

impl fmt::Display for GitHookValueError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyProductId => f.write_str("product id must not be empty"),
            Self::AbsoluteProductId { value } => {
                write!(f, "product id must not be absolute: {value}")
            }
            Self::PathSeparatorProductId { value } => {
                write!(f, "product id must not contain a path separator: {value}")
            }
            Self::ParentDirectoryProductId { value } => {
                write!(f, "product id must not contain '..': {value}")
            }
            Self::InvalidContents { field } => write!(f, "{field} must not be empty"),
            Self::ForbiddenMainContents => {
                f.write_str("main contents must not contain forbidden requirements")
            }
            Self::ClosedMainContents => f.write_str("main contents must not be closed"),
            Self::FragmentContentsMustEqual => f.write_str("fragment contents must use Equals"),
        }
    }
}

impl core::error::Error for GitHookValueError {}
