use std::{borrow::Cow, path::PathBuf};

use enum_dispatch::enum_dispatch;
use strum_macros::EnumTryAs;

use crate::input::single_input::{FileInput, SingleInput};

use super::InputTrait;

#[enum_dispatch]
pub(crate) trait MultiInputTrait: InputTrait {
    /// Get an iterator of [`SingleInput`] from this `MultiInput`.
    ///
    /// For instance, if this is a directory input, the returned inputs might correspond to the
    /// files inside that directory.
    #[allow(dead_code)]
    fn iter_inputs(&self) -> anyhow::Result<impl Iterator<Item = SingleInput>>;
}

#[derive(Clone, EnumTryAs)]
#[enum_dispatch(MultiInputTrait, InputTrait)]
pub(crate) enum MultiInput {
    /// A directory.
    Dir(DirInput),
}
impl MultiInput {
    /// Try to parse a `MultiInput` from the provided string. This might fail if the provided
    /// string cannot be parsed as a supported `MultiInput`.
    pub(crate) fn try_parse_string(input_string: &str) -> anyhow::Result<Self> {
        let metadata = std::fs::metadata(input_string);
        if metadata?.is_dir() {
            // Input is a valid directory path.
            Ok(Self::Dir(DirInput {
                path: input_string.into(),
            }))
        } else {
            anyhow::bail!(
                "Unsupported input '{}' for {}",
                input_string,
                std::any::type_name::<Self>()
            )
        }
    }
}

/// A directory.
#[derive(Clone)]
pub(crate) struct DirInput {
    /// The path pointing to the directory.
    path: PathBuf,
}
impl DirInput {
    /// An iterator of the files inside the directory, as [`FileInput`].
    pub(crate) fn iter_files(&self) -> anyhow::Result<impl Iterator<Item = FileInput>> {
        let iter = std::fs::read_dir(&self.path)?
            .flatten()
            .map(|e| e.path())
            .filter(|p| {
                p.file_name().is_some_and(|raw_name| {
                    // Skip any files with this exact name
                    raw_name != ".DS_Store" && {
                        let name = raw_name.to_string_lossy().to_lowercase();
                        // SKip all files whose names start with these characters
                        !name.starts_with("._")
                        // Skip all files whose names have these extensions
                        && !name.ends_with(".json")
                    }
                })
            })
            .filter_map(|p| FileInput::try_from_path(&p).ok());

        Ok(iter)
    }
}
impl MultiInputTrait for DirInput {
    fn iter_inputs(&self) -> anyhow::Result<impl Iterator<Item = SingleInput>> {
        Ok(self.iter_files()?.map(|file| file.into()))
    }
}
impl InputTrait for DirInput {
    fn get_identifier(&self) -> Cow<'_, str> {
        self.path
            .file_name()
            .map_or(Cow::from("<dir>"), |dir_name| dir_name.to_string_lossy())
    }
}
