use std::{borrow::Cow, collections::HashMap, ffi::OsString};

use anyhow::Result;
use log::*;
use regex::Regex;
use trillium::Status;

use crate::{
    manifest::{CaseFold, RedirectMatcher, SiteFile, SiteManifest, SiteOptions},
    redirect::RedirType,
};

/// In-memory, executable version of a site manifest.
pub struct ExecutableManifest {
    pub options: SiteOptions,
    pub error_pages: HashMap<String, SiteFile>,
    pub default_headers: HashMap<String, String>,
    files: HashMap<String, SiteFile>,
    redirects: Vec<RedirRule>,
    fallbacks: HashMap<String, String>,
}

#[derive(Debug, Clone)]
pub struct RedirRule {
    matcher: RedirMatch,
    target: String,
    rtype: RedirType,
}

#[derive(Debug, Clone)]
enum RedirMatch {
    Constant(String),
    Pattern(Regex),
}

impl ExecutableManifest {
    /// Look up an explicit redirect.
    pub fn find_redirect(&self, path: &str) -> Option<(Status, String)> {
        for redir in self.redirects.iter() {
            match &redir.matcher {
                RedirMatch::Constant(s) if s == path => {
                    return Some((redir.rtype.status(), redir.target.clone()));
                }
                RedirMatch::Pattern(p) => match p.replace(path, &redir.target) {
                    // if we replaced, the result is owend (see docs)
                    Cow::Owned(t) => return Some((redir.rtype.status(), t.into())),
                    _ => (),
                },
                _ => (),
            }
        }

        None
    }

    /// Look up the page referenced by a path.
    pub fn find_file(&self, path: &str) -> Option<&SiteFile> {
        self.files.get(path)
    }

    /// Find fallback redirect (for when we don't reference a page)
    pub fn find_fallback(&self, path: &str) -> Option<&str> {
        trace!("searching fallbacks for {}", path);
        self.fallbacks
            .get(path)
            .or_else(|| {
                if self.options.case_fold == CaseFold::Redirect {
                    let lc = path.to_lowercase();
                    trace!("searching fallbacks for case-folded {}", lc);
                    self.fallbacks.get(&lc)
                } else {
                    None
                }
            })
            .map(|s| s.as_str())
    }

    /// Add a page
    fn add_page(&mut self, path: &str, page: SiteFile) {
        let spath = if self.options.case_fold == CaseFold::Yes {
            path.to_lowercase()
        } else {
            path.to_string()
        };
        self.files.insert(spath, page);
        if self.options.case_fold == CaseFold::Redirect {
            let lc = path.to_lowercase();
            self.fallbacks.insert(lc, path.to_owned());
        }
    }

    fn add_fallback<P: AsRef<str>, T: AsRef<str>>(&mut self, path: P, tgt: T) {
        let path = path.as_ref();
        let tgt = tgt.as_ref();
        self.fallbacks.insert(path.to_owned(), tgt.to_owned());
        if self.options.case_fold == CaseFold::Redirect {
            let lc = path.to_lowercase();
            if !self.fallbacks.contains_key(&lc) {
                self.fallbacks.insert(lc, tgt.to_owned());
            }
        }
    }
}

impl TryFrom<SiteManifest> for ExecutableManifest {
    type Error = anyhow::Error;
    fn try_from(man: SiteManifest) -> Result<Self, Self::Error> {
        debug!("converting manifest to executable format");
        let mut redirects = Vec::new();
        for redir in man.redirects {
            redirects.push(RedirRule {
                matcher: match redir.matcher {
                    RedirectMatcher::Exact { equals } => RedirMatch::Constant(equals),
                    RedirectMatcher::Regex { matches } => {
                        RedirMatch::Pattern(Regex::new(&matches)?)
                    }
                },
                target: redir.target,
                rtype: redir.rtype,
            });
        }

        let mut error_pages = HashMap::new();
        for (code, path) in man.error_pages {
            if let Some(page) = man.files.get(&path) {
                error_pages.insert(code, page.clone());
            } else {
                error!("error page {} not found", path);
            }
        }

        let mut em = ExecutableManifest {
            redirects,
            options: man.options,
            default_headers: man.default_headers,
            error_pages,
            files: HashMap::new(),
            fallbacks: HashMap::new(),
        };

        for (path, page) in man.files {
            let fpath = page.file.clone();
            em.add_page(&path, page);
            if path.ends_with("/") {
                // add index.html
                em.add_fallback(format!("{}index.html", path), path.to_string());
                // add redirect from non-slash path
                em.add_fallback(path[..path.len() - 1].to_string(), path.to_string());
            } else {
                // redirect slash to non-slash
                em.add_fallback(format!("{}/", path), path.to_string());
                // if this is an html page, redirect the html extension
                if let Some(f) = fpath {
                    if f.extension() == Some(&OsString::from(".html")) && !path.ends_with(".html") {
                        em.add_fallback(format!("{}.html", path), path.to_string());
                    }
                }
            }
        }

        Ok(em)
    }
}
