use std::collections::VecDeque;

use secafs_sdk::SecAFSOptions;
use anyhow::{Context, Result as AnyhowResult};
use secafs_sdk::db::DbValue as Value;

use crate::cmd::init::open_secafs;

const ROOT_INO: i64 = 1;
const S_IFMT: u32 = 0o170000;
const S_IFDIR: u32 = 0o040000;
const S_IFREG: u32 = 0o100000;
const S_IFLNK: u32 = 0o120000;

pub async fn ls_filesystem(
    stdout: &mut impl std::io::Write,
    postgres_url: String,
    path: &str,
) -> AnyhowResult<()> {
    let options = SecAFSOptions::resolve(&postgres_url)?;
    eprintln!("Using database: {}", postgres_url);

    let secafs_inst = open_secafs(options).await?;
    let conn = secafs_inst.get_connection().await?;

    if path != "/" {
        anyhow::bail!("Only root directory (/) is currently supported");
    }

    let mut queue: VecDeque<(i64, String)> = VecDeque::new();
    queue.push_back((ROOT_INO, String::new()));

    while let Some((parent_ino, prefix)) = queue.pop_front() {
        let query = format!(
            "SELECT d.name, d.ino, i.mode FROM fs_dentry d
             JOIN fs_inode i ON d.ino = i.ino
             WHERE d.parent_ino = {}
             ORDER BY d.name",
            parent_ino
        );

        let mut rows = conn
            .query(&query, ())
            .await
            .context("Failed to query directory entries")?;

        let mut entries = Vec::new();
        while let Some(row) = rows.next().await.context("Failed to fetch row")? {
            let name: String = row
                .get_value(0)
                .ok()
                .and_then(|v| {
                    if let Value::Text(s) = v {
                        Some(s.clone())
                    } else {
                        None
                    }
                })
                .unwrap_or_default();

            let ino: i64 = row
                .get_value(1)
                .ok()
                .and_then(|v| v.as_integer().copied())
                .unwrap_or(0);

            let mode: u32 = row
                .get_value(2)
                .ok()
                .and_then(|v| v.as_integer().copied())
                .unwrap_or(0) as u32;

            entries.push((name, ino, mode));
        }

        for (name, ino, mode) in entries {
            let is_dir = mode & S_IFMT == S_IFDIR;
            let full_path = if prefix.is_empty() {
                name.clone()
            } else {
                format!("{}/{}", prefix, name)
            };

            stdout
                .write_fmt(format_args!(
                    "{} {}\n",
                    if is_dir { 'd' } else { 'f' },
                    full_path
                ))
                .context("Failed to write to stdout")?;

            if is_dir {
                queue.push_back((ino, full_path));
            }
        }
    }

    Ok(())
}

pub async fn cat_filesystem(
    stdout: &mut impl std::io::Write,
    postgres_url: String,
    path: &str,
) -> AnyhowResult<()> {
    let options = SecAFSOptions::resolve(&postgres_url)?;
    let secafs_inst = open_secafs(options).await?;

    match secafs_inst.fs.read_file(path).await? {
        Some(file) => {
            stdout.write_all(&file)?;
            Ok(())
        }
        None => anyhow::bail!("File not found: {}", path),
    }
}

pub async fn write_filesystem(
    postgres_url: String,
    path: &str,
    content: &str,
) -> AnyhowResult<()> {
    let options = SecAFSOptions::resolve(&postgres_url)?;
    let secafs_inst = open_secafs(options).await?;

    let mut components = path.split("/").collect::<Vec<_>>();
    if !path.starts_with("/") {
        components.insert(0, "");
    }
    for i in 2..components.len() {
        let dir_path = components[0..i].join("/");
        if secafs_inst.fs.stat(&dir_path).await?.is_none() {
            secafs_inst.fs.mkdir(&dir_path, 0, 0).await?;
        }
    }
    if secafs_inst.fs.stat(path).await?.is_some() {
        secafs_inst.fs.remove(path).await?;
    }
    let (_, file) = secafs_inst.fs.create_file(path, S_IFREG | 0o644, 0, 0).await?;
    file.pwrite(0, content.as_bytes()).await?;
    Ok(())
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum ChangeType {
    Added,
    Modified,
    Deleted,
}

impl std::fmt::Display for ChangeType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ChangeType::Added => write!(f, "A"),
            ChangeType::Modified => write!(f, "M"),
            ChangeType::Deleted => write!(f, "D"),
        }
    }
}

fn file_type_char(mode: u32) -> char {
    match mode & S_IFMT {
        S_IFDIR => 'd',
        S_IFLNK => 'l',
        S_IFREG => 'f',
        _ => '?',
    }
}

fn path_exists_in_base(base_path: &str, rel_path: &str) -> bool {
    let full_path = format!("{}{}", base_path, rel_path);
    std::path::Path::new(&full_path).exists()
}

pub async fn diff_filesystem(postgres_url: String) -> AnyhowResult<()> {
    let options = SecAFSOptions::resolve(&postgres_url)?;
    eprintln!("Using database: {}", postgres_url);

    let secafs_inst = open_secafs(options).await?;

    let base_path = match secafs_inst.is_overlay_enabled().await? {
        Some(path) => path,
        None => {
            println!("No diff (non-overlay filesystem)");
            return Ok(());
        }
    };

    eprintln!("Base: {}", base_path);

    let mut changes: Vec<(ChangeType, char, String)> = Vec::new();

    let delta_paths = secafs_inst.get_delta_paths().await?;
    let whiteouts = secafs_inst.get_whiteouts().await?;

    for path in &delta_paths {
        let mode = secafs_inst.get_file_mode(path).await?.unwrap_or(0);
        let type_char = file_type_char(mode);

        if path_exists_in_base(&base_path, path) {
            changes.push((ChangeType::Modified, type_char, path.clone()));
        } else {
            changes.push((ChangeType::Added, type_char, path.clone()));
        }
    }

    for path in &whiteouts {
        let full_path = format!("{}{}", base_path, path);
        let base_path_obj = std::path::Path::new(&full_path);
        let type_char = if base_path_obj.is_dir() {
            'd'
        } else if base_path_obj.is_symlink() {
            'l'
        } else if base_path_obj.is_file() {
            'f'
        } else {
            '?'
        };

        changes.push((ChangeType::Deleted, type_char, path.clone()));
    }

    changes.sort_by(|a, b| a.2.cmp(&b.2));

    if changes.is_empty() {
        println!("No changes");
    } else {
        for (change_type, type_char, path) in changes {
            println!("{} {} {}", change_type, type_char, path);
        }
    }

    Ok(())
}