use anyhow::Result;
use serde::Serialize;
use std::cmp::Reverse;
use std::io;

use crate::file_descriptor::DeviceDescriptor;
use crate::report::report_writer::ReportWriter;
use crate::types::Duplicate;
use num_traits::ToPrimitive;
use rustc_hash::FxHashMap;

pub struct JSONWriter {}

impl ReportWriter for JSONWriter {
    fn write(&mut self, duplicates: &mut Vec<Duplicate>, write: &mut dyn io::Write) -> Result<()> {
        duplicates.sort_by_key(|duplicate| {
            Reverse(duplicate.file_size * (duplicate.locations.len() as u64 - 1))
        });

        let mut known_descriptors: FxHashMap<DeviceDescriptor, u32> = FxHashMap::default();

        let total_size = duplicates
            .iter()
            .map(|d| d.file_size * (d.locations.len() as u64 - 1))
            .sum();

        let duplicates = duplicates
            .iter()
            .map(|d| {
                let total_size = (d.locations.len() as u64 - 1) * d.file_size;
                let file_size = d.file_size;

                let mut files: FxHashMap<u32, Vec<Vec<String>>> = FxHashMap::default();

                for descriptor in &d.locations {
                    let device_descriptor = descriptor.file_descriptor.device_descriptor;

                    let device = match known_descriptors.get(&device_descriptor) {
                        Some(&id) => id,
                        None => {
                            let id = known_descriptors.len().to_u32().unwrap_or(0);
                            known_descriptors.insert(device_descriptor, id);
                            id
                        }
                    };

                    let paths: Vec<String> = descriptor
                        .paths
                        .iter()
                        .map(|path| path.to_string_lossy().into_owned())
                        .collect();

                    if let Some(list) = files.get_mut(&device) {
                        list.push(paths);
                    } else {
                        let list = vec![paths];
                        files.insert(device, list);
                    }
                }

                DuplicateEntry {
                    total_size,
                    file_size,
                    files,
                }
            })
            .collect();

        let report = DuplicateReport {
            total_size,
            duplicates,
        };

        serde_json::to_writer(write, &report)?;

        Ok(())
    }
}

#[derive(Serialize)]
struct DuplicateReport {
    total_size: u64,
    duplicates: Vec<DuplicateEntry>,
}

#[derive(Serialize)]
struct DuplicateEntry {
    total_size: u64,
    file_size: u64,
    files: FxHashMap<u32, Vec<Vec<String>>>,
}
