// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use crate::metadata::now_unix_timestamp;
use env_logger::fmt::Color;
use env_logger::Target::Stdout;
use env_logger::DEFAULT_FILTER_ENV;
use log::Level;
use log::LevelFilter::{Debug, Info, Trace};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::env;
use std::fmt::Display;
use std::io::Write;
use std::ops::Deref;
use Color::{Blue, Cyan, Green, Red, Yellow};

#[derive(Default)]
enum OutputType {
    #[default]
    Logger,
    Json,
    Shell,
}

#[derive(Default)]
pub struct Logger {
    debug: bool,
    trace: bool,
    output: OutputType,
    json: RefCell<JsonOutput>,
}

#[derive(Default, Serialize, Deserialize)]
pub struct Logs {
    pub level: String,
    pub timestamp: u64,
    pub message: String,
}

#[derive(Default, Serialize, Deserialize)]
pub struct Result {
    pub code: i32,
    pub message: String,
}

#[derive(Default, Serialize, Deserialize)]
pub struct JsonOutput {
    pub logs: Vec<Logs>,
    pub result: Result,
}

impl Logger {
    pub fn create(output: &str, debug: bool, trace: bool) -> Self {
        let output_type;
        if output.eq_ignore_ascii_case("json") {
            output_type = OutputType::Json;
        } else if output.eq_ignore_ascii_case("shell") {
            output_type = OutputType::Shell;
        } else {
            output_type = OutputType::Logger;
        }
        match output_type {
            OutputType::Logger => {
                if env::var(DEFAULT_FILTER_ENV).unwrap_or_default().is_empty() {
                    let mut filter = match debug {
                        true => Debug,
                        false => Info,
                    };
                    if trace {
                        filter = Trace
                    }
                    env_logger::Builder::new()
                        .filter_module(env!("CARGO_CRATE_NAME"), filter)
                        .target(Stdout)
                        .format(|buf, record| {
                            let mut level_style = buf.style();
                            match record.level() {
                                Level::Trace => level_style.set_color(Cyan),
                                Level::Debug => level_style.set_color(Blue),
                                Level::Info => level_style.set_color(Green),
                                Level::Warn => level_style.set_color(Yellow),
                                Level::Error => level_style.set_color(Red).set_bold(true),
                            };
                            writeln!(
                                buf,
                                "{}\t{}",
                                level_style.value(record.level()),
                                record.args()
                            )
                        })
                        .try_init()
                        .unwrap_or_default();
                } else {
                    env_logger::try_init().unwrap_or_default();
                }
            }
            _ => {
                env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
                    .try_init()
                    .unwrap_or_default();
            }
        }

        Logger {
            debug,
            trace,
            output: output_type,
            json: RefCell::new(JsonOutput {
                logs: Vec::new(),
                result: Result {
                    code: 0,
                    message: "".to_string(),
                },
            }),
        }
    }

    pub fn error<T: Display>(&self, message: T) {
        self.logger(message.to_string(), Level::Error);
    }

    pub fn warn<T: Display>(&self, message: T) {
        self.logger(message.to_string(), Level::Warn);
    }

    pub fn info<T: Display>(&self, message: T) {
        self.logger(message.to_string(), Level::Info);
    }

    pub fn debug<T: Display>(&self, message: T) {
        self.logger(message.to_string(), Level::Debug);
    }

    pub fn trace<T: Display>(&self, message: T) {
        self.logger(message.to_string(), Level::Trace);
    }

    fn logger(&self, message: String, level: Level) {
        match self.output {
            OutputType::Json => {
                let trace = level <= Level::Trace && self.trace;
                let debug = level <= Level::Debug && self.debug;
                let other = level <= Level::Info;
                if trace || debug || other {
                    self.json
                        .borrow_mut()
                        .logs
                        .push(self.create_json_log(message.to_string(), level));
                }
                if level == Level::Info || level <= Level::Error {
                    self.json.borrow_mut().result.message = message;
                }
            }
            OutputType::Shell => {
                if level == Level::Info {
                    print!("{}", message);
                } else if level == Level::Error {
                    eprint!("{}", message);
                }
            }
            _ => {
                log::log!(level, "{}", message);
            }
        }
    }

    fn create_json_log(&self, message: String, level: Level) -> Logs {
        Logs {
            level: level.to_string().to_uppercase(),
            timestamp: now_unix_timestamp(),
            message,
        }
    }

    pub fn set_code(&self, code: i32) {
        self.json.borrow_mut().result.code = code;
    }

    pub fn flush(&self) {
        let json_output = &self.json.borrow();
        let json = json_output.deref();
        if !json.logs.is_empty() {
            print!("{}", serde_json::to_string_pretty(json.deref()).unwrap());
        }
    }
}
