// Copyright 2021-2022 Ian Jackson, yaahc and contributors to Hippotat and Eyre
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-Hippotat-OpenSSL-Exception
// There is NO WARRANTY.

use crate::prelude::*;

#[derive(clap::Parser,Debug)]
pub struct LogOpts {
  /// Increase debug level
  ///
  /// May be repeated for more verbosity.
  ///
  /// When using syslog, one `-D` this arranges to send to syslog even
  /// trace messages (mapped onto syslog level `DEBUG`);
  /// and two -`D` means to send to syslog even messages from lower layers
  /// (normally just the hippotat modules log to
  /// syslog).
  #[clap(long, short='D', parse(from_occurrences))]
  debug: usize,

  /// Syslog facility to use
  #[clap(long, parse(try_from_str=parse_syslog_facility))]
  syslog_facility: Option<syslog::Facility>,
}

#[throws(AE)]
fn parse_syslog_facility(s: &str) -> syslog::Facility {
  s.parse().map_err(|()| anyhow!("unrecognised syslog facility: {:?}", s))?
}

#[derive(Debug)]
struct LogWrapper<T>{
  debug: usize,
  output: T,
}

impl<T> LogWrapper<T> {
  fn wanted(&self, md: &log::Metadata<'_>) -> bool {
    let first = |mod_path| {
      let mod_path: &str = mod_path; // can't do in args as breaks lifetimes
      mod_path.split_once("::").map(|s| s.0).unwrap_or(mod_path)
    };
    self.debug >= 2 || first(md.target()) == first(module_path!())
  }

  fn set_max_level(&self) {
    log::set_max_level(if self.debug < 1 {
      log::LevelFilter::Debug
    } else {
      log::LevelFilter::Trace
    });
  }
}

impl<T> log::Log for LogWrapper<T> where T: log::Log {
  fn enabled(&self, md: &log::Metadata<'_>) -> bool {
    self.wanted(md) && self.output.enabled(md)
  }

  fn log(&self, record: &log::Record<'_>) {
    if self.wanted(record.metadata()) {
      let mut wrap = log::Record::builder();

      macro_rules! copy { { $( $f:ident ),* $(,)? } => {
        $( wrap.$f(record.$f()); )*
      } }
      copy!{
        level, target, module_path, file, line
      };
      match format_args!("{}: {}",
                         heck::AsKebabCase(record.level().as_str()),
                         record.args()) {
        args => {
          wrap.args(args);
          self.output.log(&wrap.build());
        }
      }
    }
  }

  fn flush(&self) {
    self.output.flush()
  }
}

impl LogOpts {
  #[throws(AE)]
  pub fn log_init(&self) {
    if let Some(facility) = self.syslog_facility {
      let f = syslog::Formatter3164 {
        facility,
        hostname: None,
        process: "hippotatd".into(),
        pid: std::process::id(),
      };
      let l = syslog::unix(f)
        // syslog::Error is not Sync.
        // https://github.com/Geal/rust-syslog/issues/65
        .map_err(|e| anyhow!(DisplayError(&e).to_string()))
        .context("set up syslog logger")?;
      let l = syslog::BasicLogger::new(l);
      let l = LogWrapper { output: l, debug: self.debug };
      l.set_max_level();
      let l = Box::new(l) as _;
      log::set_boxed_logger(l).context("install syslog logger")?;
    } else {
      let env = env_logger::Env::new()
        .filter("HIPPOTAT_LOG")
        .write_style("HIPPOTAT_LOG_STYLE");
  
      let mut logb = env_logger::Builder::new();
      logb.filter(Some("hippotat"),
                  *[ log::LevelFilter::Info,
                     log::LevelFilter::Debug ]
                  .get(self.debug)
                  .unwrap_or(
                    &log::LevelFilter::Trace
                  ));
      logb.parse_env(env);
      logb.init();
    }
  }
}

pub struct OptionPrefixColon<T>(pub Option<T>);
impl<T:Display> Display for OptionPrefixColon<T> {
  #[throws(fmt::Error)]
  fn fmt(&self, f: &mut fmt::Formatter) {
    if let Some(x) = &self.0 { write!(f, "{}: ", x)? }
  }
}

// For clients only, really.
pub struct Reporter<'r> {
  ic: &'r InstanceConfig,
  successes: u64,
  last_report: Option<Report>,
}

#[derive(Debug)]
struct Report {
  when: Instant,
  ok: Result<(),()>,
}         

// Reporting strategy
//   - report all errors
//   - report first success after a period of lack of messages
//   - if error, report last success

impl<'r> Reporter<'r> {
  pub fn new(ic: &'r InstanceConfig) -> Self { Reporter {
    ic,
    successes: 0,
    last_report: None,
  } }
  
  pub fn success(&mut self) {
    self.successes += 1;
    let now = Instant::now();
    if let Some(rep) = &self.last_report {
      if now - rep.when < match rep.ok {
        Ok(()) => match self.ic.success_report_interval {
          z if z == Duration::default() => return,
          nonzero => nonzero,
        },
        Err(()) => self.ic.effective_http_timeout,
      } {
        return
      }
    }
    
    info!(target:"hippotat", "{} ({}ok): running", self.ic, self.successes);
    self.last_report = Some(Report { when: now, ok: Ok(()) });
  }

  pub fn filter<T>(&mut self, req_num: Option<ReqNum>, r: Result<T,AE>)
                   -> Option<T> {
    let now = Instant::now();
    match r {
      Ok(t) => {
        Some(t)
      },
      Err(e) => {
        let m = (||{
          let mut m = self.ic.to_string();
          if let Some(req_num) = req_num {
            write!(m, " #{}", req_num)?;
          }
          if self.successes > 0 {
            write!(m, " ({}ok)", self.successes)?;
            self.successes = 0;
          }
          write!(m, ": {}", e)?;
          Ok::<_,fmt::Error>(m)
        })().unwrap();
        warn!(target:"hippotat", "{}", m);
        self.last_report = Some(Report { when: now, ok: Err(()) });
        None
      },
    }
  }
}

use backtrace::Backtrace;
use eyre::Chain;
use indenter::indented;

#[derive(Debug)]
struct EyreDedupHandler {
  backtrace: Option<Arc<parking_lot::Mutex<Backtrace>>>,
}

type DynError<'r> = &'r (dyn std::error::Error + 'static);

struct DisplayError<'r>(DynError<'r>);

impl Display for DisplayError<'_> {
  #[throws(fmt::Error)]
  fn fmt(&self, f: &mut fmt::Formatter) {
    let mut last: Option<String> = None;
    let mut error = Some(self.0);
    while let Some(e) = error {
      let m = e.to_string();
      match last {
        None => write!(f, "{}", m)?,
        Some(l) if l.contains(&m) => { },
        Some(_) => write!(f, ": {}", m)?,
      }
      last = Some(m);
      error = e.source();
    }
  }
}

impl eyre::EyreHandler for EyreDedupHandler {
  #[throws(fmt::Error)]
  fn display(&self, error: DynError, f: &mut fmt::Formatter) {
    Display::fmt(&DisplayError(error), f)?;
  }

  #[throws(fmt::Error)]
  fn debug(&self, error: DynError, f: &mut fmt::Formatter) {
    if f.alternate() {
      return core::fmt::Debug::fmt(error, f)?;
    }

    write!(f, "{}", error)?;

    if let Some(cause) = error.source() {
      write!(f, "\n\nCaused by:")?;
      let multiple = cause.source().is_some();

      for (n, error) in Chain::new(cause).enumerate() {
        writeln!(f)?;
        if multiple {
          write!(indented(f).ind(n), "{}", error)?;
        } else {
          write!(indented(f), "{}", error)?;
        }
      }
    }

    if let Some(bt) = &self.backtrace {
      let mut bt = bt.lock();
      bt.resolve();
      write!(f, "\n\nStack backtrace:\n{:?}", bt)?;
    }
  }
}

#[throws(AE)]
pub fn dedup_eyre_setup() {
  eyre::set_hook(Box::new(|_error| {
    lazy_static! {
      static ref BACKTRACE: bool = {
        match env::var("RUST_BACKTRACE") {
          Ok(s) if s.starts_with("1") => true,
          Ok(s) if s == "0" => false,
          Err(env::VarError::NotPresent) => false,
          x => {
            eprintln!("warning: RUST_BACKTRACE not understood: {:?}", x);
            false
          },
        }
      };
    }
    let backtrace = if *BACKTRACE {
      let bt = Backtrace::new_unresolved();
      let bt = Arc::new(bt.into());
      Some(bt)
    } else {
      None
    };
    Box::new(EyreDedupHandler { backtrace })
  }))
    .context("set error handler")?;
}

const MAX_WARNINGS: usize = 15;

#[derive(Debug,Default)]
pub struct Warnings {
  pub warnings: Vec<String>,
}

#[derive(Debug,Error)]
#[error("too many warnings")]
pub struct TooManyWarnings;

impl Warnings {
  #[throws(TooManyWarnings)]
  pub fn add(&mut self, e: &dyn Display) {
    if self.warnings.len() >= MAX_WARNINGS { throw!(TooManyWarnings) }
    self.warnings.push(e.to_string());
  }
}
