use std::fmt;

#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum FileType {
    Geopackage,
    Shapefile,
    Geojson,
    Excel,
    Csv,
    Parquet,
    Arrow,
    Png,
    Unknown,
}

// TODO: I should add in Avro and other common files types used in data processing/engineering
impl fmt::Display for FileType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = match self {
            FileType::Geopackage => "Geopackage",
            FileType::Shapefile => "Shapefile",
            FileType::Geojson => "GeoJSON",
            FileType::Excel => "Excel",
            FileType::Csv => "CSV",
            FileType::Parquet => "Parquet",
            FileType::Arrow => "Arrow",
            FileType::Png => "PNG",
            FileType::Unknown => "Unknown",
        };
        write!(f, "{}", name)
    }
}

const PNG_MAGIC: &[u8] = &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
const ZIP_MAGIC: &[u8] = &[0x50, 0x4B, 0x03, 0x04];
const XLS_MAGIC: &[u8] = &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
const PARQUET_MAGIC: &[u8] = &[0x50, 0x41, 0x52, 0x31];
const SQLITE_MAGIC: &[u8] = b"SQLite format 3\x00";
const ARROW_FILE_MAGIC: &[u8] = b"ARROW1";
const ARROW_CONTINUATION: &[u8] = &[0xFF, 0xFF, 0xFF, 0xFF];

pub fn detect(bytes: &[u8]) -> FileType {
    if let Some(file_type) = detect_by_magic(bytes) {
        return file_type;
    }

    if let Ok(file_type) = detect_geojson(bytes) {
        return file_type;
    }

    if looks_like_csv(bytes) {
        return FileType::Csv;
    }

    FileType::Unknown
}

pub fn detect_from_path(path: &str, bytes: &[u8]) -> FileType {
    let detected = detect(bytes);

    if detected != FileType::Unknown {
        return detected;
    }

    if let Some(extension) = std::path::Path::new(path)
        .extension()
        .and_then(|e| e.to_str())
    {
        match extension.to_lowercase().as_str() {
            "csv" => return FileType::Csv,
            "json" | "geojson" => {
                if detect_geojson(bytes).is_ok() {
                    return FileType::Geojson;
                }
            }
            _ => {}
        }
    }

    FileType::Unknown
}

fn detect_by_magic(bytes: &[u8]) -> Option<FileType> {
    if bytes.starts_with(PNG_MAGIC) {
        Some(FileType::Png)
    } else if bytes.starts_with(ZIP_MAGIC) {
        detect_zip_content(&bytes[ZIP_MAGIC.len()..])
    } else if bytes.starts_with(XLS_MAGIC) {
        Some(FileType::Excel)
    } else if bytes.starts_with(PARQUET_MAGIC) {
        Some(FileType::Parquet)
    } else if bytes.starts_with(SQLITE_MAGIC) {
        Some(FileType::Geopackage)
    } else if bytes.starts_with(ARROW_FILE_MAGIC) || is_arrow_ipc_stream(bytes) {
        Some(FileType::Arrow)
    } else {
        None
    }
}

fn detect_zip_content(bytes: &[u8]) -> Option<FileType> {
    let excel_patterns: &[&[u8]] = &[
        b"xl/worksheets",
        b"xl/_rels",
        b"docProps/",
        b"[Content_Types]",
        b"xl/workbook",
        b"xl/styles",
        b"xl/theme",
        b"xl/strings",
        b"xl/charts",
        b"xl/drawings",
        b"xl/sharedStrings",
        b"xl/metadata",
        b"xl/calc",
    ];

    let shapefile_patterns: &[&[u8]] = &[b".shp", b".dbf", b".prj", b".shx"];

    let is_excel = excel_patterns
        .iter()
        .any(|&pattern| bytes.windows(pattern.len()).any(|window| window == pattern));

    let is_shapefile = shapefile_patterns
        .iter()
        .any(|&pattern| bytes.windows(pattern.len()).any(|window| window == pattern));

    if is_excel {
        Some(FileType::Excel)
    } else if is_shapefile {
        Some(FileType::Shapefile)
    } else {
        None
    }
}

fn detect_geojson(bytes: &[u8]) -> Result<FileType, ()> {
    let text = std::str::from_utf8(bytes).map_err(|_| ())?;
    let trimmed = text.trim_start();

    if !trimmed.starts_with('{') {
        return Err(());
    }

    let lower = trimmed.to_lowercase();

    let has_type_key = lower.contains(r#""type""#) || lower.contains(r#""type" "#);
    let has_geojson_value = lower.contains(r#""featurecollection""#)
        || lower.contains(r#""feature""#)
        || lower.contains(r#""point""#)
        || lower.contains(r#""multipoint""#)
        || lower.contains(r#""linestring""#)
        || lower.contains(r#""multilinestring""#)
        || lower.contains(r#""polygon""#)
        || lower.contains(r#""multipolygon""#)
        || lower.contains(r#""geometrycollection""#);

    if has_type_key && has_geojson_value {
        Ok(FileType::Geojson)
    } else {
        Err(())
    }
}

fn looks_like_csv(bytes: &[u8]) -> bool {
    if bytes.is_empty() {
        return false;
    }

    if let Ok(text) = std::str::from_utf8(bytes) {
        let sample = if text.len() > 1000 {
            &text[..1000]
        } else {
            text
        };

        let lines: Vec<&str> = sample.lines().take(5).collect();
        if lines.is_empty() {
            return false;
        }

        let delimiter_counts: Vec<usize> =
            lines.iter().map(|line| line.matches(',').count()).collect();

        if delimiter_counts.is_empty() {
            return false;
        }

        let first_count = delimiter_counts[0];
        first_count > 0 && delimiter_counts.iter().all(|&count| count == first_count)
    } else {
        false
    }
}

fn is_arrow_ipc_stream(bytes: &[u8]) -> bool {
    if bytes.len() < 8 {
        return false;
    }

    if bytes[0..4] == [0x00, 0x00, 0x00, 0x00] && bytes[4..8] == *ARROW_CONTINUATION {
        return true;
    }

    let message_length = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
    let metadata_length = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);

    (8..0x100000).contains(&message_length)
        && metadata_length > 0
        && metadata_length < message_length
        && (message_length as usize) <= bytes.len()
        && bytes.len() > 8
        && !bytes[8..].starts_with(b"{")
        && !bytes[8..].starts_with(b"\"")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_detect_excel_xlsx() {
        let xlsx_header = vec![0x50, 0x4B, 0x03, 0x04];
        let mut bytes = xlsx_header;
        bytes.extend_from_slice(b"some data xl/worksheets more data");

        assert_eq!(detect(&bytes), FileType::Excel);
    }

    #[test]
    fn test_detect_excel_xls() {
        let xls_header = vec![0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
        assert_eq!(detect(&xls_header), FileType::Excel);
    }

    #[test]
    fn test_detect_parquet() {
        let parquet_header = vec![0x50, 0x41, 0x52, 0x31];
        assert_eq!(detect(&parquet_header), FileType::Parquet);
    }

    #[test]
    fn test_detect_geopackage() {
        let mut gpkg_header = b"SQLite format 3\x00".to_vec();
        gpkg_header.extend_from_slice(&[0; 100]);
        assert_eq!(detect(&gpkg_header), FileType::Geopackage);
    }

    #[test]
    fn test_detect_shapefile() {
        let mut shp_zip = vec![0x50, 0x4B, 0x03, 0x04];
        shp_zip.extend_from_slice(b"some data test.shp more data");
        assert_eq!(detect(&shp_zip), FileType::Shapefile);
    }

    #[test]
    fn test_detect_geojson() {
        let geojson = br#"{"type":"FeatureCollection","features":[]}"#;
        assert_eq!(detect(geojson), FileType::Geojson);
    }

    #[test]
    fn test_detect_csv() {
        let csv_data = b"name,age,city\nJohn,30,NYC\nJane,25,LA\n";
        assert_eq!(detect(csv_data), FileType::Csv);
    }

    #[test]
    fn test_detect_arrow_ipc() {
        let arrow_file = b"ARROW1\x00\x00";
        assert_eq!(detect(arrow_file), FileType::Arrow);
    }

    #[test]
    fn test_detect_arrow_ipc_stream() {
        let mut arrow_stream = vec![0x10, 0x00, 0x00, 0x00];
        arrow_stream.extend_from_slice(&[0x08, 0x00, 0x00, 0x00]);
        arrow_stream.extend_from_slice(&[0x00; 8]);
        assert_eq!(detect(&arrow_stream), FileType::Arrow);
    }

    #[test]
    fn test_detect_arrow_continuation() {
        let mut arrow_continuation = vec![0x00, 0x00, 0x00, 0x00];
        arrow_continuation.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
        assert_eq!(detect(&arrow_continuation), FileType::Arrow);
    }

    #[test]
    fn test_detect_png() {
        let png_header = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
        assert_eq!(detect(&png_header), FileType::Png);
    }

    #[test]
    fn test_detect_unknown() {
        let random_bytes = vec![0x12, 0x34, 0x56, 0x78];
        assert_eq!(detect(&random_bytes), FileType::Unknown);
    }
}
