use serde::{Deserialize, Serialize};

use crate::proc;

mod entry_serde;

#[derive(Debug, Deserialize)]
pub struct BitwardenEntry {
    pub id: String,
    pub name: String,
    #[serde(rename = "organizationId")]
    pub organization_id: Option<String>,
    #[serde(rename = "folderId")]
    pub folder_id: Option<String>,
    #[serde(rename = "collectionIds")]
    pub collection_ids: Vec<String>,

    pub fields: Option<Vec<BitwardenEntryField>>,

    pub notes: Option<String>,

    #[serde(flatten)]
    pub data: BitwardenEntryTypeData,
}

impl BitwardenEntry {
    pub fn id(&self) -> &str {
        &self.id
    }
    pub fn name(&self) -> &str {
        &self.name
    }
}

impl BitwardenEntry {
    pub fn into_command_entry(&self) -> CommandBitwardenEntry {
        CommandBitwardenEntry {
            name: self.name.clone(),
            organization_id: self.organization_id.clone(),
            folder_id: self.folder_id.clone(),
            collection_ids: self.collection_ids.clone(),
            fields: self.fields.clone(),
            notes: self.notes.clone(),
            data: self.data.clone(),
        }
    }
}

#[derive(Debug, Serialize, Clone, PartialEq)]
pub struct CommandBitwardenEntry {
    pub name: String,
    #[serde(rename = "organizationId")]
    pub organization_id: Option<String>,
    #[serde(rename = "folderId")]
    pub folder_id: Option<String>,
    #[serde(rename = "collectionIds")]
    pub collection_ids: Vec<String>,

    pub fields: Option<Vec<BitwardenEntryField>>,

    pub notes: Option<String>,

    #[serde(flatten)]
    pub data: BitwardenEntryTypeData,
}

#[derive(Debug, Clone, PartialEq)]
pub enum BitwardenEntryTypeData {
    Login(BitwardenEntryTypeLogin),
    SecureNote(BitwardenEntryTypeSecureNote),
    Card(BitwardenEntryTypeCard),
    Identity(BitwardenEntryTypeIdentity),
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BitwardenEntryTypeLogin {
    pub username: Option<String>,
    pub password: Option<String>,
    pub totp: Option<String>,
    pub uris: Option<Vec<BitwardenEntryLoginUri>>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BitwardenEntryLoginUri {
    pub uri: Option<String>,
    #[serde(rename = "match")]
    pub match_type: Option<BitwardenEntryUriMatchType>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BitwardenEntryTypeSecureNote {}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BitwardenEntryTypeCard {}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BitwardenEntryTypeIdentity {}

#[derive(
    Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Clone, Copy, PartialEq,
)]
#[repr(u8)]
pub enum BitwardenEntryFieldType {
    Text = 0,
    Hidden = 1,
    Boolean = 2,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct BitwardenEntryField {
    pub name: String,
    pub value: Option<String>,
    #[serde(rename = "type")]
    pub field_type: BitwardenEntryFieldType,
}

#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
#[repr(u8)]
pub enum BitwardenTwoStepLoginMethod {
    Authenticator = 0,
    Email = 1,
    YubiKey = 3,
}

#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Clone, PartialEq)]
#[repr(u8)]
pub enum BitwardenEntryUriMatchType {
    Domain = 0,
    Host = 1,
    StartsWith = 2,
    Exact = 3,
    RegularExpression = 4,
    Never = 5,
}

#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
#[repr(u8)]
pub enum BitwardenOrganizationUserType {
    Owner = 0,
    Admin = 1,
    User = 2,
    Manager = 3,
    Custom = 4,
}

#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
#[repr(i8)]
pub enum BitwardenOrganizationUserStatus {
    Invited = 0,
    Accepted = 1,
    Confirmed = 2,
    Revoked = -1,
}

pub struct BitwardenSession {
    session_id: Option<String>,
}

impl BitwardenSession {
    fn bw_command(&self) -> proc::Command {
        let mut cmd = proc::Command::new("bw");
        if let Some(id) = self.session_id.as_deref() {
            cmd.env_sensitive("BW_SESSION", id);
        }
        cmd
    }

    pub fn sync(&self) -> anyhow::Result<()> {
        log::info!("Syncing Bitwarden...");
        let _ = self.bw_command().arg("sync").try_spawn_to_string()?;
        Ok(())
    }

    pub fn new_or_authenticate(
        username: Option<&str>,
        bw_unlock_purpose: &str,
    ) -> anyhow::Result<Self> {
        let bw = BitwardenSession::new_if_authenticated(username, bw_unlock_purpose, true)?;
        if let Some(bw) = bw {
            bw.sync()?;
            Ok(bw)
        } else {
            log::info!("User is not logged in to bitwarden, initiating login...");
            proc::Command::new("bw")
                .arg("login")
                .stdin_inherit()
                .stderr_inherit()
                .try_spawn_to_string()?;
            // Just logged in, no point in syncing
            let bw = BitwardenSession::new_if_authenticated(username, bw_unlock_purpose, false)?;
            if let Some(bw) = bw {
                Ok(bw)
            } else {
                Err(anyhow::format_err!(
                    "Still not logged in to bitwarden, exiting..."
                ))
            }
        }
    }

    fn new_if_authenticated(
        username: Option<&str>,
        bw_unlock_purpose: &str,
        sync: bool,
    ) -> anyhow::Result<Option<Self>> {
        let status: BitwardenAuthenticationStatus = proc::Command::new("bw")
            .args(["--nointeraction", "status"])
            .try_spawn_to_json()?;

        let Some(user) = status.user() else {
            return Ok(None);
        };
        if let Some(username) = username {
            if user.user_email != username {
                return Err(anyhow::format_err!(
                    "Authenticated user in bitwarden does not match the expected user of {}, was {}",
                    username,
                    user.user_email
                ));
            }
        }
        let is_unlocked: bool = matches!(status, BitwardenAuthenticationStatus::Unlocked(_));
        if sync && !is_unlocked {
            log::info!("Syncing Bitwarden...");
            let _ = proc::Command::new("bw").arg("sync").try_spawn_to_string()?;
        }
        log::info!("Unlocking bitwarden...");
        let session_id = if is_unlocked {
            None
        } else {
            Some(
                proc::Command::new("bitwarden-unlock")
                    .args(["--purpose", bw_unlock_purpose])
                    .try_spawn_to_string()?,
            )
        };
        Ok(Some(Self { session_id }))
    }

    pub fn list_items(&self) -> anyhow::Result<Vec<BitwardenEntry>> {
        log::info!("Listing bitwarden items...");
        self.bw_command()
            // Pretty format for better error messages during json decoding issues
            .args(["--pretty", "list", "items"])
            .try_spawn_to_json()
    }

    pub fn get_item(&self, name: &str) -> anyhow::Result<Option<BitwardenEntry>> {
        let mut items = self.list_items()?;
        let Some(idx) = items
            .iter()
            .enumerate()
            .find_map(|(idx, e)| if e.name() == name { Some(idx) } else { None })
        else {
            return Ok(None);
        };
        let item = items.swap_remove(idx);
        Ok(Some(item))
    }

    pub fn get_attachment(&self, entry: &BitwardenEntry, name: &str) -> anyhow::Result<Vec<u8>> {
        self.bw_command()
            .args(["get", "attachment", name, "--itemid"])
            .arg(entry.id.as_str())
            .arg("--raw")
            .try_spawn_to_bytes()
    }

    pub fn list_own_items(&self) -> anyhow::Result<Vec<BitwardenEntry>> {
        let mut items = self.list_items()?;
        items.retain(|i| i.organization_id.as_ref().is_none_or(|o| o.is_empty()));
        Ok(items)
    }

    pub fn create_item(&self, item: &CommandBitwardenEntry) -> anyhow::Result<BitwardenEntry> {
        log::info!("Creating bitwarden entry {name}", name = item.name);
        self.bw_command()
            .args(["create", "item"])
            .stdin_json_base64(item)?
            .try_spawn_to_json()
    }

    pub fn update_item(
        &self,
        to_update: &BitwardenEntry,
        update_with: &CommandBitwardenEntry,
    ) -> anyhow::Result<()> {
        log::info!(
            "Updating bitwarden entry {name}, with id {id}",
            id = to_update.id,
            name = update_with.name
        );
        let _output = self
            .bw_command()
            .args(["edit", "item", &to_update.id])
            .stdin_json_base64(update_with)?
            .try_spawn_to_string()?;
        Ok(())
    }

    pub fn delete_item(&self, to_delete: &BitwardenEntry) -> anyhow::Result<()> {
        log::info!(
            "Deleting bitwarden entry {name}, with id: {id}",
            id = to_delete.id,
            name = to_delete.name
        );
        let _output = self
            .bw_command()
            .args(["delete", "item", &to_delete.id])
            .try_spawn_to_string()?;
        Ok(())
    }
}

impl Drop for BitwardenSession {
    fn drop(&mut self) {
        log::info!("Locking bitwarden session...");
        if self.session_id.is_some() {
            if let Err(e) = self
                .bw_command()
                .args(["--nointeraction", "lock"])
                .try_spawn_to_string()
            {
                log::error!("Could not lock bitwarden session: {e}");
            }
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(tag = "status")]
enum BitwardenAuthenticationStatus {
    #[serde(rename = "unlocked")]
    Unlocked(BitwardenAuthenticationUser),
    #[serde(rename = "locked")]
    Locked(BitwardenAuthenticationUser),
    #[serde(rename = "unauthenticated")]
    Unauthenticated,
}

impl BitwardenAuthenticationStatus {
    pub fn user(&self) -> Option<&BitwardenAuthenticationUser> {
        match self {
            Self::Locked(user) | Self::Unlocked(user) => Some(user),
            Self::Unauthenticated => None,
        }
    }
}

#[derive(Debug, Deserialize)]
struct BitwardenAuthenticationUser {
    #[serde(rename = "userEmail")]
    user_email: String,

    #[serde(rename = "userId")]
    #[allow(dead_code)]
    user_id: String,
}
