//! Site content manifets.
use std::collections::HashMap;
use std::ffi::OsString;
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::path::PathBuf;

use anyhow::{Context, Result, anyhow};
use chrono::{DateTime, Utc};
use fs_walk::WalkOptions;
use log::*;
use mime_guess::MimeGuess;
use serde::de::Visitor;
use serde::{Deserialize, Serialize};

use crate::redirect::RedirType;

/// A manifest for site content.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SiteManifest {
    /// Site name.
    #[serde(default = "default_site_name")]
    #[allow(unused)]
    pub name: String,
    /// Site options.
    #[serde(default)]
    pub options: SiteOptions,
    /// Default headers.
    #[serde(default)]
    pub default_headers: HashMap<String, String>,
    /// Error pages.
    pub error_pages: HashMap<String, String>,
    /// Map of site pages to file info.
    #[serde(default)]
    pub files: HashMap<String, SiteFile>,
    /// List of redirect rules.
    #[serde(default)]
    pub redirects: Vec<Redirect>,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct SiteOptions {
    pub case_fold: CaseFold,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum CaseFold {
    #[default]
    No,
    Yes,
    Redirect,
}

/// A single page in the manifest.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SiteFile {
    /// File path (defaults to page path).
    #[serde(default)]
    pub file: Option<PathBuf>,
    /// File content type.
    #[serde(default, rename = "type")]
    pub content_type: Option<String>,
    /// File size.
    pub size: u64,
    /// File modification time.
    pub mtime: DateTime<Utc>,
    /// File checksum (usually SHA-1 or similar hash).
    #[serde(default)]
    pub hash: Option<String>,
    /// Header overrides for this resource.
    #[serde(default)]
    pub headers: HashMap<String, String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Redirect {
    #[serde(flatten)]
    pub matcher: RedirectMatcher,
    pub target: String,
    #[serde(rename = "type", default)]
    pub rtype: RedirType,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RedirectMatcher {
    Exact { equals: String },
    Regex { matches: String },
}

impl Default for SiteManifest {
    fn default() -> Self {
        SiteManifest {
            name: default_site_name(),
            options: SiteOptions::default(),
            default_headers: Default::default(),
            error_pages: Default::default(),
            files: Default::default(),
            redirects: Default::default(),
        }
    }
}

impl<'de> Deserialize<'de> for CaseFold {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        deserializer.deserialize_any(CaseFold::No)
    }
}

impl Serialize for CaseFold {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            CaseFold::No => serializer.serialize_bool(false),
            CaseFold::Yes => serializer.serialize_bool(true),
            CaseFold::Redirect => serializer.serialize_str("redirect"),
        }
    }
}

impl<'de> Visitor<'de> for CaseFold {
    type Value = Self;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(formatter, "expecting bool or \"redirect\"")
    }

    fn visit_bool<E>(self, v: bool) -> std::result::Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(if v { Self::Yes } else { Self::No })
    }

    fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        if v.to_lowercase() == "redirect" {
            Ok(Self::Redirect)
        } else {
            Err(serde::de::Error::invalid_value(
                serde::de::Unexpected::Str(v),
                &self,
            ))
        }
    }
}

/// Load a manifest from a file.
pub fn load_manifest(path: &Path) -> Result<SiteManifest> {
    let file = fs::File::open(path)?;
    let manifest: SiteManifest = serde_json::from_reader(file)
        .with_context(|| format!("failed to parse manifest {}", path.display()))?;
    Ok(manifest)
}

/// Generate a site manifest from a tree of files.
pub fn generate_manifest(root: &Path) -> Result<SiteManifest> {
    debug!("generating manifest from {}", root.display());
    let walk = WalkOptions::new().files().walk(root);

    let mut pages = HashMap::new();
    for path in walk {
        let path = path?;
        let stat = fs::metadata(&path)?;
        let rpath = path.strip_prefix(&root)?;
        let guess = MimeGuess::from_path(&path);
        trace!("found path: {}", path.display());
        let mut page = SiteFile {
            file: None,
            content_type: guess.first().map(|m| m.to_string()),
            hash: None,
            size: stat.size(),
            mtime: stat.modified()?.into(),
            headers: HashMap::default(),
        };

        let mut url = if rpath.file_name() == Some(&OsString::from("index.html")) {
            page.file = Some(rpath.to_path_buf());
            let rp2 = rpath.parent();
            rp2.ok_or_else(|| anyhow!("path {:?} has no parent", rpath))?
                .to_str()
                .ok_or_else(|| anyhow!("path {:?} is not valid Unicode", rpath))?
                .to_string()
                + "/"
        } else {
            rpath
                .to_str()
                .ok_or_else(|| anyhow!("path {:?} is not valid Unicode", rpath))?
                .to_string()
        };
        if url != "/" {
            url.insert(0, '/');
        }
        pages.insert(url, page);
    }

    Ok(SiteManifest {
        name: "Generated Site".into(),
        options: SiteOptions::default(),
        error_pages: Default::default(),
        default_headers: Default::default(),
        files: pages,
        redirects: Default::default(),
    })
}

fn default_site_name() -> String {
    "Overpowered Site".into()
}
