use crate::{json_result::JsonResult, model::register::Register};
use printpdf::{Mm, PdfDocument};
use rocket::{http::Status, serde::json::Json};
use serde::{Deserialize, Serialize};
use std::{
    f32::consts::E,
    fs::{self, File},
    io::BufWriter,
    path::PathBuf,
    result,
    time::{SystemTime, UNIX_EPOCH},
};

const SCAN_PATH: &str = "/srv/srv/shares/eegsac/ESCANEOS/";

/// Represents the result of parsing an eegsac URL
#[derive(Serialize, Deserialize)]
pub enum ScanInfo {
    /// The url has both DNI & id
    Full(String, i32, String),
    /// The url only has a DNI
    Partial(String, String),
    /// Te url is invalid
    Error(String),
}

#[get("/scans/detect")]
pub async fn detect_scans() -> (Status, Json<JsonResult<Vec<(PathBuf, ScanInfo)>>>) {
    let files = match get_valid_files() {
        Ok(f) => f,
        Err(err) => {
            eprintln!("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<JsonResult<Vec<(PathBuf, ScanInfo)>>>,
) -> (Status, Json<JsonResult<()>>) {
    let data = data.into_inner();
    let data = match data {
        JsonResult::Ok(v) => v,
        JsonResult::Error(err) => {
            return (
                Status::BadRequest,
                JsonResult::err("Se recibió un error.".into()),
            )
        }
    };

    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) => {
            eprintln!("Error reading dir: {:?}", err);
            return Err("Error leyendo directorio".into());
        }
    };

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

    /*
        TODO: The system will detect info from an image.
        If successful, that image will be renamed to:
        eeg_<unix_timestamp>.jpg
        and the timestamp will be sent to the frontend
        along with the detected info.

        Then the frontend will send back the list of timestamps & info
        and the backend will do the conversion.

        This way, we can ensure that only the images sent to & from the FE
        can be converted.
    */

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

        let file_path = p.path();

        if file_path.is_dir() {
            continue;
        }

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

        let filename = match filename.to_str() {
            Some(p) => p,
            None => {
                eprintln!("Error getting file as string: {:?}", p);
                return Err("Error leyendo nombre de archivo de escaneo".into());
            }
        };

        if filename.ends_with(".jpg") && filename.starts_with("eeg") {
            result.push(file_path);
        };
    }

    Ok(result)
}

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

fn get_image_info(path: PathBuf) -> ScanInfo {
    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) => {
            eprintln!("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();

    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);

    // get qr from cropped image
    let results = bardecoder::default_decoder().decode(&cropped_img);

    if results.is_empty() {
        eprintln!("QR not found");
        return ScanInfo::Error("QR no encontrado.".into());
    }

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

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

    match url.find('?') {
        Some(p) => {
            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) => {
                            eprintln!("Error renombrando archivo: {:?}", err);

                            ScanInfo::Error("Error renombrando archivo.".into())
                        }
                    }
                }
                Err(_) => ScanInfo::Error("QR invalido: El iid no es un número.".into()),
            }
        }
        None => {
            // 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) => {
                    eprintln!("Error renombrando archivo: {:?}", err);

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

// TODO: this should return a list of files that succeeded & failed
async fn convert_scans_from_data(data: &Vec<(PathBuf, ScanInfo)>) -> Result<(), String> {
    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
        },
    );

    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);

    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) => {
            eprintln!("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, timestamp),
                    None => {
                        log::error!("Register not found in data from DB: {} {}", 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, timestamp),
                    None => {
                        log::error!("Register not found in data from DB: {}", dni);
                        continue;
                    }
                }
            }
            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();

        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)
            }
            Err(reason) => {
                log::error!("Error converting to pdf: {}", reason);
                continue;
            }
        }
    }

    Ok(())
}

fn convert_to_pdf(image_path: &PathBuf, output_path: &PathBuf) -> Result<(), String> {
    let img = match image::open(image_path) {
        Ok(i) => i,
        Err(err) => {
            log::error!("Error opening image: {:?}", err);
            return Err(format!("Error abriendo imagen"));
        }
    };

    // Rotate image 45 degrees clockwise
    // let img = img.rotate270();

    // Generate PDF
    let (doc, page1, layer1) =
        // PdfDocument::new("Certificado - EEGSAC", Mm(297.0), Mm(210.0), "Layer 1");
        PdfDocument::new("Certificado - EEGSAC", Mm(210.0), Mm(297.0), "Layer 1");

    let current_layer = doc.get_page(page1).get_layer(layer1);

    let pdf_image = printpdf::image::Image::from_dynamic_image(&img);
    let img_transform = printpdf::ImageTransform::default();

    /*
    This section of code assumes that the image is scaned at 200 DPI.
    IF the image is scanned at a different DPI, this will give
    wrong results.
     */
    let img_transform = printpdf::ImageTransform {
        dpi: Some(200.0),
        ..img_transform
    };

    pdf_image.add_to_layer(current_layer, img_transform);

    let result = doc.save(&mut BufWriter::new(File::create(output_path).unwrap()));

    // TODO: Delete image
    match result {
        Ok(_) => Ok(()),
        Err(reason) => {
            log::error!("Error saving pdf: {:?}", reason);
            Err(format!("Error guardando pdf"))
        }
    }
}
