use crate::models::Modes;
use anyhow::{Context, Ok};
use cardwire_core::gpu::{Gpu, GpuBlocker, is_gpu_blocked};
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, io};
const CONFIG_PATH: &str = "/etc/cardwire";
const STATE_PATH: &str = "/var/lib/cardwire";

#[allow(dead_code)]
enum FileKind {
    Config,
    GpuState,
    ModeState,
    PciState,
}
// TODO: Handle fs error for tomorrow
#[derive(Deserialize, Serialize)]
pub struct CardwireConfig {
    auto_apply_gpu_state: bool,
    block_nvidia_vulkan: bool,
}

impl CardwireConfig {
    /// Read TOML config file and return it's settings as a struct
    // TODO: Error handling on std::fs
    pub fn build() -> anyhow::Result<CardwireConfig> {
        let config_file = format!("{}/cardwire.toml", CONFIG_PATH);
        Self::parse_config(&config_file)
    }
    /// Parse the .toml file into a CardwireConfig
    fn parse_config(config_file: &str) -> anyhow::Result<CardwireConfig> {
        if !(fs::exists(config_file)?) {
            Self::create_default_config().context("Could not create default dir for config")?;
        }
        let config_content =
            fs::read_to_string(config_file).context("Could not read cardwire.toml")?;
        Ok(toml::from_str(&config_content).context("Failed to parse the toml config")?)
    }
    /// Create a default cardwire.toml if not present
    fn create_default_config() -> anyhow::Result<()> {
        create_default_file(FileKind::Config)?;
        Ok(())
    }
    /// Return block_vulkan value
    pub fn block_nvidia_vulkan(&self) -> bool {
        self.block_nvidia_vulkan
    }
    pub fn auto_apply_gpu_state(&self) -> bool {
        self.auto_apply_gpu_state
    }
}

// This is the easiest way i found to have a good looking json, might change later
#[derive(Serialize, Deserialize)]
pub struct CardwireGpuState {
    gpu: HashMap<String, CardwireGpuUnit>,
}
// A gpu contain a pci and a blocked state,
// TODO: A more precise way to identify a GPU, but not dangerous since cardwire does not block
// default gpu
#[derive(Serialize, Deserialize)]
struct CardwireGpuUnit {
    block: bool,
}

impl CardwireGpuState {
    /// Build a CardwireGpuState struct
    pub fn build() -> anyhow::Result<CardwireGpuState> {
        let state_file = format!("{STATE_PATH}/gpu_state.json");

        let gpu_hash = Self::parse_gpu_state(&state_file);
        if let Err(e) = gpu_hash {
            warn!("gpu_hash.json could not get parsed {e}, overwriting with default one...");
            Self::create_default_state()?;
        }
        let gpu_hash = Self::parse_gpu_state(&state_file).context("couldn't fix gpu_hash.json")?;
        let gpu_state = CardwireGpuState { gpu: gpu_hash };
        Ok(gpu_state)
    }
    // Parse directly into CardwireGpuState
    fn parse_gpu_state(state_file: &str) -> anyhow::Result<HashMap<String, CardwireGpuUnit>> {
        if !(fs::exists(state_file)?) {
            Self::create_default_state().context("Could not create default gpu_state.json")?;
        }
        let gpu_state = fs::read_to_string(state_file)
            .with_context(|| format!("Could not read file {}", state_file))?;

        let content: HashMap<String, CardwireGpuUnit> =
            serde_json::from_str(&gpu_state).context("Could not parse string into json")?;
        Ok(content)
    }
    /// Create default gpu_state.json, including folders if missing
    fn create_default_state() -> anyhow::Result<()> {
        create_default_file(FileKind::GpuState)?;
        Ok(())
    }
    /// Save the new state into the daemon and to the gpu_state.json file
    pub async fn save_state(
        &mut self,
        gpu_list: &HashMap<usize, Gpu>,
        blocker: &GpuBlocker,
    ) -> anyhow::Result<()> {
        // Prevent overwriting default config if it's not replaceable
        if self.gpu.contains_key("Null") {
            info!("detected default gpu_state file, overwriting it...");
            self.gpu.clear();
        }
        // Save to daemon state
        for gpu in gpu_list.values() {
            self.gpu.insert(
                gpu.pci.clone(),
                CardwireGpuUnit {
                    block: is_gpu_blocked(blocker, gpu)?,
                },
            );
        }
        // Save the whole hashmap into json
        let state_file = serde_json::to_string_pretty(&self.gpu)?;
        tokio::fs::write(format!("{STATE_PATH}/gpu_state.json"), state_file).await?;
        Ok(())
    }
    /// return true if it was generated by default func
    pub fn is_default_state(&self) -> bool {
        self.gpu.contains_key("Null")
    }
    /// search key in gpu hashmap,
    pub fn gpu_block_state(&self, pci: &String) -> bool {
        match self.gpu.get_key_value(pci) {
            Some(value) => value.1.block,
            None => false,
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct CardwireModeState {
    mode: Modes,
}
impl CardwireModeState {
    /// Read a mode.json file and return into a struct
    pub fn build() -> anyhow::Result<CardwireModeState> {
        let mode_file = format!("{STATE_PATH}/mode.json");

        let mode = Self::parse_mode_state(&mode_file);
        if let Err(e) = mode {
            warn!("mode.json could not get parsed {e}, overwriting with default one...");
            Self::create_default_mode()?;
        }
        let mode = Self::parse_mode_state(&mode_file).context("couldn't fix mode.json")?;
        Ok(mode)
    }
    fn parse_mode_state(mode_file: &str) -> anyhow::Result<CardwireModeState> {
        if !(fs::exists(mode_file)?) {
            Self::create_default_mode()?;
        }
        let mode_state = fs::read_to_string(mode_file)?;
        let string_content: CardwireModeState =
            serde_json::from_str(&mode_state).context("Failed to parse json to str")?;
        Ok(string_content)
    }
    fn create_default_mode() -> anyhow::Result<()> {
        create_default_file(FileKind::ModeState)?;
        Ok(())
    }
    pub fn mode(&self) -> Modes {
        self.mode
    }
    /// Save the new mode into the daemon and to the mode_state.json file
    pub async fn save_state(&mut self, new_mode: Modes) -> anyhow::Result<()> {
        // Save to daemon state
        self.mode = new_mode;
        // Save the whole state into the json
        let state_file = serde_json::to_string_pretty(&self)?;
        tokio::fs::write(format!("{STATE_PATH}/mode.json"), state_file).await?;
        Ok(())
    }
}
/// Helper function to create default file, used for all config struct
fn create_default_file(kind: FileKind) -> anyhow::Result<()> {
    let result = match kind {
        FileKind::Config => {
            create_default_folder(FileKind::Config)
                .context("could not create default folder for cardwire.toml")?;
            // Default config for cardwire
            // TODO: Move to default trait?
            let default_config = toml::to_string_pretty(&CardwireConfig {
                auto_apply_gpu_state: true,
                block_nvidia_vulkan: false,
            })?;
            // write
            fs::write(format!("{}/cardwire.toml", CONFIG_PATH), default_config)
        }
        FileKind::GpuState => {
            create_default_folder(FileKind::GpuState)
                .context("could not create default folder for gpu_state.json")?;
            // Default gpu_state for cardwire
            // TODO: Move to default trait?
            let mut defaut_hash: HashMap<String, CardwireGpuUnit> = HashMap::new();
            let _ = defaut_hash.insert("Null".to_string(), CardwireGpuUnit { block: false });
            let default_gpu_state = serde_json::to_string_pretty(&defaut_hash)?;
            // write
            fs::write(format!("{}/gpu_state.json", STATE_PATH), default_gpu_state)
        }
        FileKind::ModeState => {
            create_default_folder(FileKind::ModeState)
                .context("could not create default folder for mode.json")?;
            // Default mode for cardwire
            // TODO: Move to default trait?
            let default_state = CardwireModeState {
                mode: Modes::Manual,
            };
            let default_mode_state = serde_json::to_string_pretty(&default_state)?;
            // write
            fs::write(format!("{}/mode.json", STATE_PATH), default_mode_state)
        }
        FileKind::PciState => {
            create_default_folder(FileKind::PciState)
                .context("could not create default folder for pci_state.json")?;
            // Default pci_state for cardwire, not implemented yet
            // TODO: Move to default trait?
            let default_state = r#"{}"#;
            fs::write(format!("{}/pci_state.json", STATE_PATH), default_state)
        }
    };
    // Handle the fs error here
    let result: anyhow::Result<()> = match result {
        std::result::Result::Ok(()) => Ok(()),
        std::result::Result::Err(e) => match e.kind() {
            io::ErrorKind::PermissionDenied => return Err(e.into()),
            io::ErrorKind::IsADirectory => return Err(e.into()),
            io::ErrorKind::ReadOnlyFilesystem => return Err(e.into()),
            // happen if in: /var/lib/cardwire/gpu_state.json
            // cardwire is a file and not a directory
            io::ErrorKind::NotADirectory => return Err(e.into()),
            // ignore this one
            io::ErrorKind::AlreadyExists => Ok(()),
            // if directory not found, try to create again
            io::ErrorKind::NotFound => create_default_folder(kind),
            _ => Ok(()),
        },
    };
    result
}
/// Create all folders cardwire need
fn create_default_folder(kind: FileKind) -> anyhow::Result<()> {
    let directory = match kind {
        FileKind::Config => CONFIG_PATH,
        _ => STATE_PATH,
    };
    // fs error that should make the daemon exit
    if let Err(e) = fs::create_dir_all(directory) {
        let _ = match e.kind() {
            io::ErrorKind::PermissionDenied => return Err(e.into()),
            io::ErrorKind::ReadOnlyFilesystem => return Err(e.into()),
            io::ErrorKind::NotADirectory => return Err(e.into()),
            _ => Ok(()),
        };
    }
    Ok(())
}
