use crate::permissions::Perm;
use crate::Config;
use axum::{
    debug_handler,
    extract::{Query, State},
    response::{IntoResponse, Response},
    Json,
};
use axum_extra::extract::SignedCookieJar;
use libpijul::{
    pristine::{ChangeId, Position, Vertex},
    vertex_buffer::VertexBuffer,
    Base32, ChannelTxnT, TxnT, TxnTExt,
};
use serde_derive::*;
use tracing::*;

#[derive(Debug, Serialize, Deserialize)]
pub struct PosQuery {
    channel: Option<String>,
    pos: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct Tree {
    owner: String,
    repo: String,
    channel: String,
    channels: Vec<String>,
    can_delete: bool,
    inode: Inode,
    #[serde(skip_serializing_if = "Option::is_none")]
    hled: Option<String>,
    token: String,
    login: Option<String>,
}

#[derive(Debug, Serialize)]
pub enum Inode {
    File {
        path: Vec<super::PathElement>,
        file: Option<String>,
    },
    Directory {
        path: Vec<super::PathElement>,
        children: Vec<Directory>,
    },
}

#[derive(Debug, Serialize)]
pub struct Directory {
    is_dir: bool,
    meta: libpijul::pristine::InodeMetadata,
    name: String,
    pos: String,
}

#[debug_handler]
pub async fn tree(
    State(config): State<Config>,
    jar: SignedCookieJar,
    tree: crate::repository::RepoPath,
    token: axum_csrf::CsrfToken,
    pos: Query<PosQuery>,
) -> Result<Response, crate::Error> {
    debug!("tree {:?}", tree);
    let (uid, login) = crate::get_user_login_(&jar, &config).await?;
    let mut db = config.db.get().await?;
    let (id, _) = super::repository_id(&mut db, &tree.owner, &tree.repo, uid, Perm::READ).await?;

    let c = super::channel_spec_id(id, tree.channel.as_deref().unwrap_or("main"));
    debug!("channel {:?}", c);
    let locks = config.repo_locks.clone();
    let repo_ = locks.get(&id).await.unwrap();
    let channels = repo_.channels().await;
    let channel = tree.channel.unwrap_or_else(|| "main".to_string());
    let channel_ = channel.clone();
    let inode = tokio::task::spawn_blocking(move || {
        let pristine = repo_.pristine.blocking_read();
        let txn = pristine.txn_begin()?;
        let channel = if let Some(channel) = txn.load_channel(&c)? {
            channel
        } else if txn.channels("")?.is_empty() && (pos.channel.as_deref() == Some("main") || pos.channel.is_none()) {
            return Ok(Inode::Directory {
                path: Vec::new(),
                children: Vec::new(),
            });
        } else {
            debug!("channel not found {:?}", c);
            if tracing::enabled!(tracing::Level::DEBUG) {
                for c in txn.channels("")? {
                    let c = c.read();
                    debug!("channel: {:?}", c.name);
                }
            }
            return Err(crate::Error::ChannelNotFound { channel: channel_ });
        };
        let ch = channel.read();
        let pos = pos
            .pos
            .as_ref()
            .map(String::as_bytes)
            .and_then(Position::from_base32);
        let pos = pos.unwrap_or(Position::ROOT);

        if !txn.is_alive(&*ch, &pos.inode_vertex())? {
            debug!("not alive {:?}", pos.inode_vertex());
            return Err(crate::Error::InodeNotFound);
        }

        let current_path = super::current_path(&txn, &*ch, &repo_.changes, pos)?;
        debug!("current_path = {:?}", current_path);
        let is_dir = if let Some(path) = current_path.last() {
            debug!("last path = {:?}", path);
            path.meta.is_dir()
        } else {
            true
        };

        if is_dir {
            let mut children = Vec::new();
            for x in libpijul::fs::iter_graph_children(&txn, &repo_.changes, txn.graph(&*ch), pos)?
            {
                let (pos, _, meta, name) = x?;
                children.push(Directory {
                    is_dir: meta.is_dir(),
                    meta,
                    pos: pos.to_base32(),
                    name,
                })
            }
            children.sort_by(|a, b| (!a.is_dir, &a.name).cmp(&(!b.is_dir, &b.name)));
            Ok(Inode::Directory {
                path: current_path,
                children,
            })
        } else {
            let mut out = RawVertexBuf { out: Vec::new() };
            let txn = libpijul::ArcTxn::new(txn);
            libpijul::output::output_file(&repo_.changes, &txn, &channel, pos, &mut out)
                .map_err(|_| crate::Error::Txn)?;
            Ok(Inode::File {
                path: current_path,
                file: String::from_utf8(out.out).ok(),
            })
        }
    })
    .await??;

    let t = Tree {
        owner: tree.owner,
        repo: tree.repo,
        channel,
        channels,
        can_delete: true,
        inode,
        hled: None,
        token: token.authenticity_token()?,
        login,
    };
    debug!("{:?}", t);
    Ok((token, Json(t)).into_response())
}

#[derive(Debug)]
struct RawVertexBuf {
    out: Vec<u8>,
}

impl VertexBuffer for RawVertexBuf {
    fn output_line<E, C: FnOnce(&mut [u8]) -> Result<(), E>>(
        &mut self,
        v: Vertex<ChangeId>,
        contents: C,
    ) -> Result<(), E> {
        let len = self.out.len();
        self.out.resize(len + (v.end - v.start), 0);
        contents(&mut self.out[len..])?;
        Ok(())
    }
    fn output_conflict_marker<T>(
        &mut self,
        marker: &str,
        _: usize,
        _: Option<(&T, &[&libpijul::Hash])>,
    ) -> Result<(), std::io::Error> {
        self.out.extend(marker.as_bytes());
        Ok(())
    }
}