use std::error::Error;
use std::{fs, io};
use std::net::{IpAddr, Ipv6Addr};
use std::os::unix;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use actix_web::{web, App, HttpResponse, HttpServer, get, Responder, HttpRequest};
use log::{LevelFilter};
use notify::{Event, EventKind, RecursiveMode, Watcher};
use notify::event::{AccessKind, AccessMode};
use systemd_journal_logger::{connected_to_journal, JournalLog};
use serde::{Deserialize, Serialize};


macro_rules! info {
    ($msg:tt) => {
        match connected_to_journal() {
            true => log::info!("[INFO] {}", $msg),
            false => println!("[INFO] {}", $msg),
        }
    };
}

macro_rules! warn {
    ($msg:tt) => {
        match connected_to_journal() {
            true => log::warn!("[WARN] {}", $msg),
            false => println!("[WARN] {}", $msg),
        }
    };
}

macro_rules! error {
    ($msg:tt) => {
        match connected_to_journal() {
            true => log::error!("[ERROR] {}", $msg),
            false => eprintln!("[ERROR] {}", $msg),
        }
    };
}

#[derive(Clone, Serialize, Deserialize)]
struct DomainLinkConfig {
    domains: Vec<String>,
    target: String,
}

#[derive(Clone, Serialize, Deserialize, Default)]
struct UserConfig {
    domain_configs: Vec<DomainLinkConfig>
}

#[derive(Clone, Serialize, Deserialize)]
struct SystemConfig {
    ports: Vec<u16>,
    addresses: Vec<IpAddr>,
}

impl Default for SystemConfig {
    fn default() -> Self {
        Self {
            ports: vec![8000],
            addresses: vec![IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0))],
        }
    }
}

#[derive(Clone)]
struct Config {
    user: UserConfig,
    system: SystemConfig,
}

impl Config {
    pub fn load() -> Result<(Self, Vec<PathBuf>), Box<dyn Error>> {
        // get list of home directories
        // query every home directory for a config file (just attempt a load, an empty config is perfectly fine)
        // merge all configs into one
        let mut directories: Vec<PathBuf> = vec![];
        let mut user_config = UserConfig::default();
        let root_contents = match fs::read_dir("/") {
            Ok(contents) => { contents }
            Err(e) => {
                error!(e);
                return Err(Box::new(e));
            }
        };

        root_contents.for_each(|directory| {
            let path = directory.expect("Unexpected Error while Unwrapping the listed Dir Entry").path();
            if path.is_dir() {
                match path.display().to_string().as_str() {
                    "/root" => {
                        let (mut root_configs, root_path) = Self::load_user_config_directory(path);
                        if let Some(root_directory) = root_path {
                            directories.push(Path::new(root_directory.as_str()).to_path_buf());
                        }
                        user_config.domain_configs.append(&mut root_configs.domain_configs);
                    },
                    "/home" => {
                        match fs::read_dir(path) {
                            Ok(home_contents) => {
                                home_contents.for_each(|home_folder| {
                                    let home_folder_path = home_folder.expect("Unexpected Error while Unwrapping the listed Home Dir Entry").path();
                                    if home_folder_path.is_dir() {
                                        let (mut user_folder_configs, user_folder_path) = Self::load_user_config_directory(home_folder_path);
                                        if let Some(user_folder_directory) = user_folder_path {
                                            directories.push(Path::new(user_folder_directory.as_str()).to_path_buf());
                                        }
                                        user_config.domain_configs.append(&mut user_folder_configs.domain_configs);
                                    }
                                })
                            },
                            Err(e) => {
                                error!(e);
                            }
                        }
                    },
                    _ => {}
                }
            }
        });

        let etc_path = format!("/etc/{}", env!("CARGO_PKG_NAME"));
        let usr_path = format!("/usr/local/share/{}", env!("CARGO_PKG_NAME"));

        let system_config_paths = vec![
            Path::new(&etc_path),
            Path::new(&usr_path),
        ];

        let mut system_path= system_config_paths[1];

        for path in system_config_paths {
            let cfg_path = path.join("config.toml");
            if cfg_path.exists() {
                system_path = path;
                break;
            }
        };

        let path = format!("{}/config.toml", system_path.display());
        match confy::load_path(path.clone()) {
            Ok(data) => {
                let msg = format!("Using {}", path);
                directories.push(system_path.to_path_buf());
                info!(msg);
                Self::check_for_duplicate_domains(&user_config.domain_configs);
                Ok((Config {
                    user: user_config,
                    system: data,
                }, directories))
            },
            Err(e) => {
                error!(e);
                Err(Box::new(e))
            }
        }
    }

    fn load_user_config_directory(path: PathBuf) -> (UserConfig, Option<String>) {
        let config_path = format!("{}/.config/{}", path.display(), env!("CARGO_PKG_NAME"));
        match confy::load_path::<UserConfig>(config_path.clone() + "/domains.toml") {
            Ok(data) => {
                if data.domain_configs.is_empty() {
                    match Self::fix_path_ownership(path, vec![".config", env!("CARGO_PKG_NAME"), "domains.toml"]) {
                        Ok(_) => (),
                        Err(e) => {
                            error!(e);
                        }
                    };
                }
                else {
                    let msg = format!("Using {config_path}/domains.toml");
                    info!(msg);
                }
                (data, Some(config_path))
            },
            Err(e) => {
                match &e {
                    confy::ConfyError::GeneralLoadError(os_error) => {
                        if os_error.raw_os_error() == Some(13) {
                            let msg = format!("Missing read permissions for {}, skipping", path.display().to_string().as_str());
                            warn!(msg);
                            (UserConfig::default(), None)
                        }
                        else {
                            error!(e);
                            (UserConfig::default(), None)
                        }
                    },
                    _ => {
                        error!(e);
                        (UserConfig::default(), None)
                    }
                }
            }
        }
    }

    fn fix_path_ownership(root: PathBuf, paths: Vec<&str>) -> io::Result<()> {
        let root_metadata = fs::metadata(&root)?;
        let uid = root_metadata.uid();
        let gid = root_metadata.gid();
        match paths.len() {
            1 => {
                let new_root = root.join(paths[0]);
                unix::fs::chown(new_root, Some(uid), Some(gid))
            },
            _ => {
                let new_root = root.join(paths[0]);
                let ret = unix::fs::chown(&new_root, Some(uid), Some(gid));

                let mut new_paths = paths.clone();
                new_paths.remove(0);
                match Self::fix_path_ownership(new_root, new_paths) {
                    Ok(_) => ret,
                    Err(e) => {
                        error!(e);
                        Err(e)
                    }
                }
            }
        }
    }

    fn check_for_duplicate_domains(domain_configs: &[DomainLinkConfig]) {
        let mut checked_domains: Vec<String> = vec![];
        for (cfg_idx, config) in domain_configs.iter().enumerate() {
            for (idx, domain) in config.domains.iter().enumerate() {
                if !checked_domains.contains(domain) {
                    if config.domains[idx+1..].contains(domain) {
                        // Error
                        let msg = format!("Duplicate Domain use detected for '{domain}");
                        warn!(msg);
                        checked_domains.push(domain.clone());
                    }
                    else {
                        for d in domain_configs[cfg_idx+1..].iter() {
                            if d.domains.contains(domain) {
                                let msg = format!("Duplicate Domain use detected for '{domain}");
                                warn!(msg);
                                checked_domains.push(domain.clone());
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
}

#[actix_web::main]
async fn main() -> notify::Result<()> {
    JournalLog::new().expect("Systemd-Logger crate error").install().expect("Systemd-Logger crate error");
    log::set_max_level(LevelFilter::Info);

    let (config, directories) = Config::load().expect("Error while loading or generating the config");

    let loaded_redirects_msg = format!("Loaded {} redirects from user config", config.user.domain_configs.len());
    info!(loaded_redirects_msg);

    let app_data = web::Data::new(RwLock::new(config));
    let app_data_clone = web::Data::clone(&app_data);

    let mut server = HttpServer::new(move || {
        App::new()
            .app_data(web::Data::clone(&app_data_clone))
            .service(status)
            .service(do_redirect)
    });

    for address in app_data.read().expect("Read Lock Failed").system.addresses.iter() {
        let ports = app_data.read().expect("Read Lock Failed").system.ports.clone();
        for port in ports.iter() {
            let msg = if address.is_ipv6() {
                format!("Listening on [{address}]:{port}")
            }
            else {
                format!("Listening on {address}:{port}")
            };
            info!(msg);
            server = server.bind((*address, *port))?
        }
    }

    let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
        match res {
            Ok(event) => {
                if event.kind == EventKind::Access(AccessKind::Close(AccessMode::Write)) {
                    let (config, _) = Config::load().expect("Error while loading or generating the config");
                    let mut tmp_app_data = app_data.write().expect("Write Lock Failed");
                    tmp_app_data.system = config.system;
                    tmp_app_data.user = config.user;
                    info!("Reloading Configuration");
                }
            },
            Err(e) => {
                let msg = format!("Error watching files: {e}");
                error!(msg);
            }
        }
    })?;

    for directory in directories.iter() {
        watcher.watch(directory, RecursiveMode::NonRecursive)?;
    }

    let _ = server.run().await;

    Ok(())
}

#[get("/")]
async fn do_redirect(data: web::Data<RwLock<Config>>, request: HttpRequest) -> impl Responder {
    let redirects = &data.read().expect("Read Lock Failed").user.domain_configs;
    if let Some(host_raw) = request.headers().get("host") {
        let host = host_raw.to_str().expect("host conversion to string should never fail");
        for redirect in redirects.iter() {
            if redirect.domains.contains(&host.to_owned()) {
                return HttpResponse::PermanentRedirect().insert_header(("location", redirect.target.to_string().as_str())).finish();
            }
        }
        let fail_msg = format!("No Redirect for {host} found");
        return HttpResponse::NotFound().body(fail_msg)
    }
    HttpResponse::NotFound().body("Host not specified")
}

#[get("/status")]
async fn status(data: web::Data<RwLock<Config>>) -> impl Responder {
    let redirects = &data.read().expect("Read Lock Failed").user.domain_configs;
    let mut body_msg = format!("Redirects Loaded: {}", redirects.len());
    for redirect in redirects.iter() {
        body_msg += "\n[";
        for (idx, domain) in redirect.domains.iter().enumerate() {
            body_msg += domain;
            if idx != (redirect.domains.len() - 1) {
                body_msg += ", ";
            }
        }
        body_msg += format!("] => '{}'", redirect.target).as_str();
    }

    HttpResponse::Ok().body(body_msg)
}