use std::{
    fs::Permissions,
    io::Write,
    os::unix::fs::PermissionsExt,
    path::{Path, PathBuf},
    str::FromStr,
};

use tempfile::NamedTempFile;
use time::{
    OffsetDateTime, PrimitiveDateTime,
    format_description::{FormatItem, well_known::Rfc2822},
    macros::format_description,
    parsing::Parsable,
};

use radicle::{
    Profile, Storage,
    cob::ObjectId,
    prelude::{NodeId, RepoId},
    storage::ReadStorage,
};

use crate::ergo::Oid;

pub fn lookup_repo(profile: &Profile, wanted: &str) -> Result<(RepoId, String), UtilError> {
    let storage = Storage::open(profile.storage(), profile.info()).map_err(UtilError::storage)?;

    let repos = storage.repositories().map_err(UtilError::repositories)?;
    let mut rid = None;

    if let Ok(wanted_rid) = RepoId::from_urn(wanted) {
        for ri in repos {
            let project = ri
                .doc
                .project()
                .map_err(|e| UtilError::project(ri.rid, e))?;

            if ri.rid == wanted_rid {
                if rid.is_some() {
                    return Err(UtilError::DuplicateRepositories(wanted.into()));
                }
                rid = Some((ri.rid, project.name().to_string()));
            }
        }
    } else {
        for ri in repos {
            let project = ri
                .doc
                .project()
                .map_err(|e| UtilError::project(ri.rid, e))?;

            if project.name() == wanted {
                if rid.is_some() {
                    return Err(UtilError::DuplicateRepositories(wanted.into()));
                }
                rid = Some((ri.rid, project.name().to_string()));
            }
        }
    }

    if let Some(rid) = rid {
        Ok(rid)
    } else {
        Err(UtilError::NotFound(wanted.into()))
    }
}

pub fn oid_from_cli_arg(profile: &Profile, rid: RepoId, commit: &str) -> Result<Oid, UtilError> {
    if let Ok(oid) = Oid::from_str(commit) {
        Ok(oid)
    } else {
        lookup_commit(profile, rid, commit)
    }
}

pub fn lookup_nid(profile: &Profile) -> Result<NodeId, UtilError> {
    Ok(*profile.id())
}

pub fn lookup_commit(profile: &Profile, rid: RepoId, gitref: &str) -> Result<Oid, UtilError> {
    let storage = Storage::open(profile.storage(), profile.info()).map_err(UtilError::storage)?;
    let repo = storage
        .repository(rid)
        .map_err(|e| UtilError::repo_open(rid, e))?;
    let object = repo
        .backend
        .revparse_single(gitref)
        .map_err(|e| UtilError::rev_parse(gitref, e))?;

    Ok(object.id().into())
}

pub fn now() -> Result<String, UtilError> {
    let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
    OffsetDateTime::now_utc()
        .format(fmt)
        .map_err(UtilError::time_format)
}

pub fn parse_timestamp(timestamp: &str) -> Result<OffsetDateTime, UtilError> {
    const SIMPLIFIED_ISO8601_WITH_Z: &[FormatItem<'static>] =
        format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");

    fn parse_one(
        timestamp: &str,
        fmt: &(impl Parsable + ?Sized),
    ) -> Result<OffsetDateTime, time::error::Parse> {
        let r = PrimitiveDateTime::parse(timestamp, fmt);
        if let Ok(t) = r {
            Ok(t.assume_utc())
        } else {
            #[allow(clippy::unwrap_used)]
            Err(r.err().unwrap())
        }
    }

    if let Ok(t) = parse_one(timestamp, SIMPLIFIED_ISO8601_WITH_Z) {
        Ok(t)
    } else {
        Err(UtilError::TimestampParse(timestamp.into()))
    }
}

pub fn rfc822_timestamp(ts: &OffsetDateTime) -> Result<String, UtilError> {
    let ts = ts.format(&Rfc2822).map_err(UtilError::time_format)?;
    Ok(ts.to_string())
}

pub fn read_file_as_string(filename: &Path) -> Result<String, UtilError> {
    String::from_utf8(
        std::fs::read(filename).map_err(|err| UtilError::Readfile(filename.into(), err))?,
    )
    .map_err(|err| UtilError::Utf8(filename.into(), err))
}

pub fn read_file_as_objectid(filename: &Path) -> Result<ObjectId, UtilError> {
    let s = read_file_as_string(filename)?;
    ObjectId::from_str(s.trim()).map_err(|err| UtilError::read_object_id(filename, err))
}

pub fn safely_overwrite<P: AsRef<Path>>(filename: P, data: &[u8]) -> Result<(), UtilError> {
    let filename = filename.as_ref();
    let dirname = filename
        .parent()
        .ok_or(UtilError::NoParent(filename.to_path_buf()))?;
    let mut tmp = NamedTempFile::new_in(dirname)
        .map_err(|err| UtilError::CreateTemp(dirname.to_path_buf(), err))?;
    tmp.write_all(data)
        .map_err(|err| UtilError::WriteTemp(dirname.to_path_buf(), err))?;
    let mode = Permissions::from_mode(0o644);
    std::fs::set_permissions(tmp.path(), mode).map_err(UtilError::TempPerm)?;
    tmp.persist(filename)
        .map_err(|err| UtilError::rename_temp(filename, err))?;
    Ok(())
}

#[derive(Debug, thiserror::Error)]
pub enum UtilError {
    #[error("failed to look up open node storage")]
    Storage(#[source] Box<radicle::storage::Error>),

    #[error("failed to list repositories in node storage")]
    Repositories(#[source] Box<radicle::storage::Error>),

    #[error("failed to look up project info for repository {0}")]
    Project(RepoId, #[source] Box<radicle::identity::doc::PayloadError>),

    #[error("node has more than one repository called {0}")]
    DuplicateRepositories(String),

    #[error("node has no repository called: {0}")]
    NotFound(String),

    #[error("failed to open git repository in node storage: {0}")]
    RepoOpen(RepoId, #[source] Box<radicle::storage::RepositoryError>),

    #[error("failed to parse git ref as a commit id: {0}")]
    RevParse(String, #[source] Box<radicle::git::raw::Error>),

    #[error("failed to format time stamp")]
    TimeFormat(#[source] Box<time::error::Format>),

    #[error("failed to parse timestamp {0:?}")]
    TimestampParse(String),

    #[error("failed to read file {0}")]
    Readfile(PathBuf, #[source] std::io::Error),

    #[error("failed to convert file to UTF8: {0}")]
    Utf8(PathBuf, #[source] std::string::FromUtf8Error),

    #[error("failed to read object id from {0}")]
    ReadObjectId(PathBuf, #[source] Box<radicle::cob::object::ParseObjectId>),

    #[error("file name to write to doesn't have a parent directory: {0}")]
    NoParent(PathBuf),

    #[error("failed to create temporary file in directory {0}")]
    CreateTemp(PathBuf, #[source] std::io::Error),

    #[error("failed to write to temporary file in {0}")]
    WriteTemp(PathBuf, #[source] std::io::Error),

    #[error("failed to set permissions on temporary file")]
    TempPerm(#[source] std::io::Error),

    #[error("failed to rename temporary file to {0}")]
    RenameTemp(PathBuf, #[source] Box<tempfile::PersistError>),
}

impl UtilError {
    fn storage(err: radicle::storage::Error) -> Self {
        Self::Storage(Box::new(err))
    }

    fn repositories(err: radicle::storage::Error) -> Self {
        Self::Repositories(Box::new(err))
    }

    fn project(repo_id: RepoId, err: radicle::identity::doc::PayloadError) -> Self {
        Self::Project(repo_id, Box::new(err))
    }

    fn repo_open(repo_id: RepoId, err: radicle::storage::RepositoryError) -> Self {
        Self::RepoOpen(repo_id, Box::new(err))
    }

    fn rev_parse(s: &str, err: radicle::git::raw::Error) -> Self {
        Self::RevParse(s.into(), Box::new(err))
    }

    fn time_format(err: time::error::Format) -> Self {
        Self::TimeFormat(Box::new(err))
    }

    fn read_object_id(path: &Path, err: radicle::cob::object::ParseObjectId) -> Self {
        Self::ReadObjectId(path.into(), Box::new(err))
    }

    fn rename_temp(path: &Path, err: tempfile::PersistError) -> Self {
        Self::RenameTemp(path.into(), Box::new(err))
    }
}
