use crate::model::register::ScanData;
use crate::{json_result::JsonResult, model::register::Register};
use rocket::{http::Status, serde::json::Json};
use serde::{Deserialize, Serialize};
use std::process::Command;
use std::{
    fs,
    path::PathBuf,
    time::{SystemTime, UNIX_EPOCH},
};

// TODO: Move to ENV
const SCAN_PATH: &str = "/srv/srv/shares/eegsac/ESCANEOS/";

/// Represents the result of parsing a QR code, and look for an eegsac URL.
/// 
/// When the frontend requests the list of files available for conversion, this
/// is sent. It contains essentially a unix timestamp
#[derive(Serialize, Deserialize)]
pub enum ScanInfo {
    /// The url has both DNI & id.
    /// The fields are: DNI, id, timestamp
    Full(String, i32, String),
    /// The url only has a DNI
    /// The fields are: DNI, timestamp
    Partial(String, String),
    /// There was an error getting the QR code.
    /// The field is: timestamp
    Empty(String),
    /// The url is invalid
    /// The field is: reason
    Error(String),
}

/// Reads files from the `ESCANEOS` folder, and returns those that follow the rules
#[get("/scans/detect")]
pub async fn detect_scans() -> (Status, Json<JsonResult<Vec<ScanInfo>>>) {
    let files = match get_valid_files() {
        Ok(f) => f,
        Err(err) => {
            log::error!("Error getting valid files: {:?}", err);
            return (Status::InternalServerError, JsonResult::err(err));
        }
    };

    let file_details = get_details_from_paths(files);

    (Status::Ok, JsonResult::ok(file_details))
}

#[options("/scans/convert")]
pub fn convert_scans_options() -> Status {
    Status::Ok
}

#[post("/scans/convert", format = "json", data = "<data>")]
pub async fn convert_scans(data: Json<Vec<ScanInfo>>) -> (Status, Json<JsonResult<()>>) {
    let data = data.into_inner();

    match convert_scans_from_data(&data).await {
        Ok(_) => (Status::Ok, JsonResult::ok(())),
        Err(reason) => (Status::InternalServerError, JsonResult::err(reason)),
    }
}

/// Reads the SCAN_PATH directory and returns a list of file paths
/// that are valid for processing.
fn get_valid_files() -> Result<Vec<PathBuf>, String> {
    let paths = match fs::read_dir(SCAN_PATH) {
        Ok(paths) => paths,
        Err(err) => {
            log::error!("Error reading dir: {:?}", err);
            return Err("Error leyendo directorio".into());
        }
    };

    let mut result = Vec::<PathBuf>::new();

    for p in paths {
        let p = match p {
            Ok(p) => p,
            Err(err) => {
                log::error!("Error reading path: {:?}", err);
                return Err("Error recuperando archivo de lista de archivos".into());
            }
        };

        let file_path = p.path();

        // Ignore directories
        if file_path.is_dir() {
            continue;
        }

        let filename = match file_path.file_name() {
            Some(filename) => filename,
            None => {
                log::error!("Error getting file name: {:?}", file_path);
                return Err("Error leyendo archivo en directorio".into());
            }
        };

        // This may fail because filenames may contain non UTF-8 characters
        let filename = match filename.to_str() {
            Some(p) => p,
            None => {
                log::error!("Error getting file as string: {:?}", p);
                return Err("Error leyendo nombre de archivo de escaneo".into());
            }
        };

        // The rules for the filenames
        if filename.starts_with("eeg") && filename.ends_with(".jpg") {
            result.push(file_path);
        };
    }

    Ok(result)
}

/// Detects the QR code data from every file
fn get_details_from_paths(paths: Vec<PathBuf>) -> Vec<ScanInfo> {
    paths.into_iter().map(|path| get_image_info(path)).collect()
}

/// Actually detects the QR data from a file.
/// Reads the file, looks for a QR & return info about this process
/// 
/// Here 3 things may happen:
/// 
/// - QR found
/// 
///     The URL is parsed to get the person DNI & certificate id.
/// 
///     The file in disk is renamed to `eeg_<timestamp>.jpg`
/// 
///     The timestamp, DNI & cert id (if found) are sent to FE
/// 
/// - QR not found
/// 
///     The file in disk is renamed to `eeg_<timestamp>.jpg`, and this timestamp is sent to FE 
/// 
/// - Error finding QR
/// 
///     An error is returned
/// 
/// ## File renaming
/// 
/// The processed files are renamed to `eeg_<timestamp>.jpg` and sent to the FE.
/// 
/// Later on, the FE will send back this information, and another function in this
/// file will use the timestamps to read the JPGs, and do the conversion to PDF.
/// 
/// This is done so that there's no race conditions. If 2 persons call this function at
/// the same time, only the last one will have the valid timestamps.
fn get_image_info(path: PathBuf) -> ScanInfo {
    // Open the image
    let img = image::open(&path).unwrap();

    // get unix timestamp now in ms
    let current_time = SystemTime::now().duration_since(UNIX_EPOCH);
    let current_time = match current_time {
        Ok(t) => t,
        Err(err) => {
            log::error!("Error obteniendo hora actual: {:?}", err);
            return ScanInfo::Error("Error obteniendo hora actual: TIME WENT BACKWARDS.".into());
        }
    };
    let current_ms = current_time.as_millis();

    // Get width & height
    let width = img.width();
    let height = img.height();

    // Calculate where to crop the image, such that only 1/8 is used
    let third_point_height = (height / 4) * 3;
    let mid_point_width = width / 2;
    let remaining_height = height - third_point_height;

    // crop image
    let cropped_img = img.crop_imm(0, third_point_height, mid_point_width, remaining_height);

    // Apply thresholding
    let thresh_img = cropped_img.to_luma8();
    let thresholded_image = imageproc::contrast::threshold(&thresh_img, 128);
    let thresholded_image = image::DynamicImage::ImageLuma8(thresholded_image);

    // get qr from thresholded image.
    // if the image is not thresholded, it may fail more often
    let results = bardecoder::default_decoder().decode(&thresholded_image);


    // If no QR is detected, only rename
    if results.is_empty() {
        log::info!("QR not found");

        // Rename file
        let mut new_path = path.clone();
        new_path.set_file_name(format!("eeg_{}.jpg", current_ms));

        return match fs::rename(path, new_path) {
            Ok(_) => ScanInfo::Empty(current_ms.to_string()),
            Err(err) => {
                log::error!("Error renombrando archivo: {:?}", err);

                ScanInfo::Error("Error renombrando archivo.".into())
            }
        };
    }

    let url = match &results[0] {
        Ok(url) => url,
        Err(reason) => {
            log::error!("Error decoding qr: {:?}", reason);
            return ScanInfo::Error("Error recuperando QR.".into());
        }
    };

    // URL must have format `https://eegsac.com/certificado/<dni>?iid=<id>`
    // or                   `https://eegsac.com/certificado/<dni>`

    // Try to get DNI & iid
    match url.find('?') {
        Some(p) => {
            // Here, an iid is found. Extract it & return

            let dni_length = p - 31;
            let equals_pos = match url.find('=') {
                Some(p) => p,
                None => {
                    return ScanInfo::Error(
                        "QR invalido: Tiene signo de interrogacion (?) pero no igual (=).".into(),
                    )
                }
            };

            let dni = url.chars().skip(31).take(dni_length).collect::<String>();
            let iid = url.chars().skip(equals_pos + 1).collect::<String>();

            match iid.parse() {
                Ok(v) => {
                    // Rename file
                    let mut new_path = path.clone();
                    new_path.set_file_name(format!("eeg_{}.jpg", current_ms));

                    match fs::rename(path, new_path) {
                        Ok(_) => ScanInfo::Full(dni, v, current_ms.to_string()),
                        Err(err) => {
                            log::error!("Error renombrando archivo: {:?}", err);

                            ScanInfo::Error("Error renombrando archivo.".into())
                        }
                    }
                }
                Err(_) => ScanInfo::Error("QR invalido: El iid no es un número.".into()),
            }
        }
        None => {
            // Here the URL has only a DNI, no iid

            // Rename file
            let mut new_path = path.clone();
            new_path.set_file_name(format!("eeg_{}.jpg", current_ms));

            match fs::rename(path, new_path) {
                Ok(_) => ScanInfo::Partial(url.chars().skip(31).collect(), current_ms.to_string()),
                Err(err) => {
                    log::error!("Error renombrando archivo: {:?}", err);

                    ScanInfo::Error("Error renombrando archivo.".into())
                }
            }
        }
    }
}


/// Converts a list of files into PDFs.
/// 
/// Uses the timestamps inside `data` to read the correct JPG files and convert them.
async fn convert_scans_from_data(data: &Vec<ScanInfo>) -> Result<(), String> {
    // Get a tuple with all the DNIs & iids.
    let (ids, dnis) = data.iter().fold(
        (vec!["-1".to_string()], vec!["''".to_string()]),
        |mut acc, next| {
            match next {
                ScanInfo::Full(_, id, _) => acc.0.push(id.to_string()),
                ScanInfo::Partial(dni, _) => acc.1.push(format!("'{}'", dni)),
                _ => (),
            };

            acc
        },
    );

    // Concatenate the iids & dnis
    let register_id_list = ids.join(",");
    let person_dni_list = dnis.join(",");

    log::info!("register_id_list: {}", register_id_list);
    log::info!("person_dni_list: {}", person_dni_list);

    // This method expects the concatenated dnis & iids to build a single SQL query,
    // instead of making a request for each file.
    // Returns a list of information about each cert: person names, cert name
    let persons_info = Register::get_course_and_person(register_id_list, person_dni_list).await;

    let persons_info = match persons_info {
        Ok(v) => v,
        Err(err) => {
            log::error!("Error getting persons info: {:?}", err);
            return Err("Error obteniendo información de personas.".into());
        }
    };

    // Match the results from DB with the data from the frontend,
    // & convert the files to PDFs
    for info in data {
        let (person_info, timestamp) = match info {
            ScanInfo::Full(dni, id, timestamp) => {
                let person_info = persons_info.iter().find(|e| e.register_id == *id);
                match person_info {
                    Some(p) => (p.clone(), timestamp),
                    None => {
                        log::error!("Register not found in data from DB: id {}, dni {}", id, dni);
                        continue;
                    }
                }
            }
            ScanInfo::Partial(dni, timestamp) => {
                let person_info = persons_info
                    .iter()
                    .find(|e| e.person_dni == *dni && e.register_id == 0);
                match person_info {
                    Some(p) => (p.clone(), timestamp),
                    None => {
                        log::error!("Register not found in data from DB: dni {}", dni);
                        continue;
                    }
                }
            }
            ScanInfo::Empty(timestamp) => {
                // This is dummy data, to convert files without a QR
                let scan_data = ScanData {
                    course_name: "pendiente".into(),
                    person_names: "EEG".into(),
                    person_paternal_surname: "".into(),
                    person_maternal_surname: "".into(),
                    person_dni: "".into(),
                    register_id: 0,
                };

                (scan_data, timestamp)
            }
            ScanInfo::Error(reason) => {
                log::info!("Found an error while matching data with DB: {}", reason);
                continue;
            }
        };

        let current_time = SystemTime::now().duration_since(UNIX_EPOCH);
        let current_time = match current_time {
            Ok(t) => t,
            Err(err) => {
                log::error!("Error obteniendo hora actual: {:?}", err);
                continue;
            }
        };
        let current_ms = current_time.as_millis();

        // Create the resulting filename
        let filename = format!(
            "{} {} {} - {} [{}][{:X}].pdf",
            person_info.person_names,
            person_info.person_paternal_surname,
            person_info.person_maternal_surname,
            person_info.course_name,
            current_ms,
            person_info.register_id,
        );

        let image_path = PathBuf::from(format!("{}eeg_{}.jpg", SCAN_PATH, timestamp));
        let output_path = PathBuf::from(format!("{}{}", SCAN_PATH, filename));

        match convert_to_pdf(&image_path, &output_path) {
            Ok(_) => {
                log::info!("Converted file: {}", filename);
                // Delete image
                match fs::remove_file(image_path) {
                    Ok(_) => log::info!("Deleted image for: {}", filename),
                    Err(err) => {
                        log::error!("Error deleting image for {}: {:?}", filename, err);
                    }
                }
            }
            Err(reason) => {
                log::error!("Error converting to pdf: {}", reason);
                continue;
            }
        }
    }

    Ok(())
}

/// Converts a file to PDF using Imagemagick
fn convert_to_pdf(image_path: &PathBuf, output_path: &PathBuf) -> Result<(), String> {
    // Just use imagemagick to convert the image to pdf
    // -interlace Plane -gaussian-blur 0.05 -quality 75% -rotate 270 doc.jpg doc.jpg.pdf
    let call = Command::new("convert")
        .arg("-strip")
        .arg("-interlace")
        .arg("Plane")
        .arg("-gaussian-blur")
        .arg("0.05")
        .arg("-quality")
        .arg("75%")
        // .arg("-rotate")
        // .arg("270")
        .arg(image_path)
        .arg(output_path)
        .spawn();

    let mut call = match call {
        Ok(r) => r,
        Err(err) => {
            log::error!("Error starting imagemagick: {:?}", err);
            return Err(format!("Error convirtiendo imagen"));
        }
    };

    match call.wait() {
        Ok(exit_status) if exit_status.success() => Ok(()),
        Ok(exit_status) => {
            log::error!("Error calling imagemagick: {:?}", exit_status);
            Err(format!(
                "Error ejecutando imagemagick: codigo de salida no exitoso"
            ))
        }
        Err(err) => {
            log::error!("Error calling imagemagick: {:?}", err);
            Err(format!("Error ejecutando imagemagick"))
        }
    }
}
