use log::*;

use threadpool::ThreadPool;

use colored::*;
use linkify::{LinkFinder, LinkKind};

use std::fs::File;
use std::io::prelude::*;

use reqwest::header;
use std::time::Duration;

use std::borrow::Cow;
use std::sync::Arc;
use std::sync::Mutex;

use std::sync::atomic::Ordering;

use rustc_hash::{FxHashMap, FxHashSet};

enum UrlStatus {
   Unknown,
   UrlOk,
   UrlError(String),
}

struct HashVal {
   paths: FxHashSet<String>,
   status: UrlStatus,
}

type UrlHash = FxHashMap<String, HashVal>;

pub struct LinkCheck {
   pool: Mutex<ThreadPool>,
   urlhash: Arc<Mutex<UrlHash>>,
   print_all: bool,
}

impl LinkCheck {
   pub fn new(num_threads: usize, print_all: bool) -> LinkCheck {
       // TODO not sure if I really need this
       // so I will remove it and see it this will cause problems
       // unsafe {
       //     openssl_probe::init_openssl_env_vars();
       // }
       let pool = Mutex::new(ThreadPool::new(num_threads));
       LinkCheck {
           pool,
           urlhash: Arc::new(Mutex::new(FxHashMap::default())),
           print_all,
       }
   }

   pub fn check_urls(&self, fname: &str) {
       let print_all = self.print_all;
       if let Some(links) = get_links(fname) {
           for l in links {
               let urlhash = self.urlhash.clone();
               let fname_s = String::from(fname);
               self.pool.lock().unwrap().execute(move || {
                   check_link(&l, &fname_s, &urlhash, print_all);
               });
           }
       }
   }
}

impl Drop for LinkCheck {
   fn drop(&mut self) {
       //println!("Now dropping ...");
       let pool = self.pool.lock().unwrap();
       pool.join();
   }
}

fn check_link(url: &str, fname: &str, urlhash: &Arc<Mutex<UrlHash>>, print_all: bool) {
   let url = String::from(url);

   let mut run_check_link = false;

   // It is very important to keep the lock for the urlhash
   // only for a short period of time
   //
   // If we don't find the url in the urlhash then
   // we set `run_check_link` to `true` so that we will
   // check the url
   {
       let f = String::from(fname);

       let mut urlhash = urlhash.lock().unwrap();
       if !urlhash.contains_key(&url) {
           let mut hs = FxHashSet::default();
           hs.insert(f);
           let url1 = url.clone();

           urlhash.insert(
               url1,
               HashVal {
                   status: UrlStatus::Unknown,
                   paths: hs,
               },
           );
           run_check_link = true;
       } else if let Some(hs) = urlhash.get_mut(&url) {
           match &hs.status {
               UrlStatus::Unknown => {
                   hs.paths.insert(f);
               }
               UrlStatus::UrlOk => {
                   if print_all {
                       print_ok(super::ARGS.no_colors, &url, &f);
                   };
               }
               UrlStatus::UrlError(e) => {
                   e0022!(f, e);
               }
           }
       }
   }

   //
   if run_check_link {
       match check_link_inner(&url, true) {
           UrlStatus::UrlOk => {
               let mut urlhash = urlhash.lock().unwrap();
               if let Some(hs) = urlhash.get_mut(&url) {
                   if print_all {
                       for p in hs.paths.iter() {
                           print_ok(super::ARGS.no_colors, &url, p);
                       }
                   }
                   hs.status = UrlStatus::UrlOk;
               }
           }
           UrlStatus::UrlError(e) => {
               let mut urlhash = urlhash.lock().unwrap();
               if let Some(hs) = urlhash.get_mut(&url) {
                   for p in hs.paths.iter() {
                       e0022!(p, e);
                   }
                   hs.status = UrlStatus::UrlError(e);
               }
           }
           _ => (),
       }
   }
}

fn get_links(fname: &str) -> Option<Vec<String>> {
   let fhdl = File::open(fname);
   match fhdl {
       Ok(mut f) => {
           let mut buf = Vec::new();

           match f.read_to_end(&mut buf) {
               Ok(_bytes_read) => {
                   return get_links_inner(&String::from_utf8_lossy(&buf));
               }
               Err(e) => error!("Error reading file {}: {:?}", fname, e),
           }
       }
       Err(e) => error!("Error opening file {}: {}", fname, e),
   }

   None
}

fn resolve_entities(url: &str) -> Cow<'static, str> {
   if !url.contains('&') {
       return String::from(url).into();
   }

   let v = vec![("&ouml;", "ö"), ("&uuml;", "ü")];
   let mut url_new = String::from(url);
   for (e, r) in v {
       url_new = url_new.replace(e, r);
   }
   url_new.into()
}

// retrieves links in a string and then checks those links
fn get_links_inner(s: &str) -> Option<Vec<String>> {
   let mut finder = LinkFinder::new();
   finder.kinds(&[LinkKind::Url]);

   // finder.links() does the actual search for URLs
   let links: Vec<_> = finder.links(s).collect();
   let result: Vec<&str> = links.iter().map(|e| e.as_str()).collect();

   let mut links = vec![];
   for r in result {
       if !r.starts_with("http://") && !r.starts_with("https://") && !r.starts_with("ftp://") {
           continue;
       }

       // This is a workaround to prevent URLs ending with certain characters
       let url = resolve_entities(r.trim_end_matches(['。', '`']));

       links.push(url.into());
   }
   if !links.is_empty() {
       Some(links)
   } else {
       None
   }
}

fn check_link_inner(l: &str, head: bool) -> UrlStatus {
   let mut headers = header::HeaderMap::new();
   headers.insert(
       header::USER_AGENT,
       header::HeaderValue::from_static(
           "Mozilla/5.0 (X11; Linux i686; rv:64.0) Gecko/20100101 Firefox/64.0",
       ),
   );

   let default_policy = reqwest::redirect::Policy::default();
   let policy = reqwest::redirect::Policy::custom(move |attempt| {
       if attempt.url().host_str() == Some("127.0.0.1") {
           attempt.stop()
       } else {
           default_policy.redirect(attempt)
       }
   });

   let cb = reqwest::blocking::Client::builder()
       .gzip(true)
       .redirect(policy)
       .default_headers(headers)
       .timeout(Duration::from_secs(7))
       .build()
       .unwrap();
   // let url: Url =
   // match l.parse() {
   //    Ok(url) => url,
   //    Err(e) => {  println!("Error: {:?}", e); panic!("Scheiss"); }
   // };
   let resp = if head {
       cb.head(l).send()
   } else {
       cb.get(l).send()
   };
   match resp {
       Ok(s) => {
           if s.status().is_informational()
               || s.status().is_success()
               || s.status().is_redirection()
           {
               return UrlStatus::UrlOk;
           }

           if head {
               check_link_inner(l, false)
           } else {
               let e = format!("{}: {}", l, s.status());
               UrlStatus::UrlError(e)
           }
       }
       Err(e) => {
           let e = format!("{}", e);
           UrlStatus::UrlError(e)
       }
   }
}

fn print_ok(no_colors: bool, url: &str, f: &str) {
   if no_colors {
       info!("✔    {} in {}", &url, f);
   } else {
       //        println!("✔    {} in {}", &url, f);
       info!("{}    {} in {}", "✔".bright_green().bold(), url, f);
   }
}