//! Data types for output format and format version choice.
//!
//! These data types express the values of the `--output-format` and
//! `--output-version` global options to `sq`.

use std::io::Write;

use anyhow::{anyhow, Result};

pub mod pluralize;
pub mod sanitize;
pub mod wrapping;

pub use keyring::ListItem as KeyringListItem;
pub use wkd::WkdUrlVariant;

use crate::cli::output::{OutputFormat, OutputVersion};

pub const DEFAULT_OUTPUT_VERSION: OutputVersion = OutputVersion::new(0, 0, 0);
pub const OUTPUT_VERSIONS: &[OutputVersion] = &[OutputVersion::new(0, 0, 0)];

/// A model for the output of `sq` subcommands.
///
/// This is for adding machine-readable output (such as JSON) to
/// subcommand. Every subcommand is represented as a variant, for each
/// version of the output. Versioning is global. We keep the latest
/// subversion of each major version.
///
/// Each variant is created by a dedicated function.
pub enum Model {
    KeyringListV0(keyring::ListV0),
    WkdUrlV0(wkd::UrlV0),
}

impl Model {

    fn version(v: Option<OutputVersion>) -> OutputVersion {
        v.unwrap_or(DEFAULT_OUTPUT_VERSION)
    }

    /// Create a model for the output of `sq wkd url` and `sq wkd
    /// direct-url` subcommands.
    pub fn wkd_url(version: Option<OutputVersion>,
                   variant: wkd::WkdUrlVariant,
                   advanced_url: String,
                   direct_url: String) -> Result<Self> {
        let version = Self::version(version);
        let result = match version {
            wkd::UrlV0::V => Self::WkdUrlV0(wkd::UrlV0::new(variant, advanced_url, direct_url)),
            _ => return Err(anyhow!("unknown output version {:?}", version)),
        };
        Ok(result)
    }

    /// Create a model for the output of the `sq keyring list`
    /// subcommand.
    pub fn keyring_list(version: Option<OutputVersion>, certs: Vec<keyring::ListItem>, all_uids: bool) -> Result<Self> {
        let version = Self::version(version);
        let result = match version {
            keyring::ListV0::V => Self::KeyringListV0(keyring::ListV0::new(certs, all_uids)),
            _ => return Err(anyhow!("unknown output version {:?}", version)),
        };
        Ok(result)
    }

    /// Write the output of a model to an open write handle in the
    /// format requested by the user.
    pub fn write(&self, format: OutputFormat, w: &mut dyn Write) -> Result<()> {
        match self {
            Self::KeyringListV0(x) => {
                match format {
                    OutputFormat::Json => x.json(w)?,
                    _ => x.human_readable(w)?,
                }
            }
            Self::WkdUrlV0(x) => {
                match format {
                    OutputFormat::Json => x.json(w)?,
                    _ => x.human_readable(w)?,
                }
            }
        }
        Ok(())
    }
}

/// Serializes an object to JSON.
pub fn to_json<O, W>(mut w: W, o: &O) -> Result<()>
where
    O: serde::Serialize,
    W: std::io::Write,
{
    // Pretty-print, then add a final newline.
    serde_json::to_writer_pretty(&mut w, o)?;
    writeln!(w)?;
    Ok(())
}


// Model output as a data type that can be serialized.
mod keyring {
    use sequoia_openpgp as openpgp;
    use openpgp::{
        Result,
        cert::Cert,
    };
    use crate::Config;
    use super::{OutputVersion, Write};
    use serde::Serialize;

    #[derive(Debug, Serialize)]
    pub struct ListV0 {
        #[serde(skip)]
        all_uids: bool,
        sq_output_version: OutputVersion,
        keys: Vec<ListItem>,
    }

    impl ListV0 {
        pub const V: OutputVersion = OutputVersion::new(0, 0, 0);

        pub fn new(keys: Vec<ListItem>, all_uids: bool) -> Self {
            Self {
                all_uids,
                sq_output_version: Self::V,
                keys,
            }
        }

        pub fn human_readable(&self, w: &mut dyn Write) -> Result<()> {
            for (i, item) in self.keys.iter().enumerate() {
                match item {
                    ListItem::Error(e) => {
                        writeln!(w, "{}. {}", i, e)?;
                    },
                    ListItem::Cert(cert) => {
                        let line = format!("{}. {}", i, cert.fingerprint);
                        let indent = line.chars().map(|_| ' ').collect::<String>();
                        write!(w, "{}", line)?;
                        match &cert.primary_userid {
                            Some(uid) => writeln!(w, " {}", uid)?,
                            None => writeln!(w)?,
                        }
                        if self.all_uids {
                            for uid in &cert.userids {
                                writeln!(w, "{} {}", indent, uid)?;
                            }
                        }
                    }
                }
            }
            Ok(())
        }

        pub fn json(&self, w: &mut dyn Write) -> Result<()> {
            super::to_json(w, self)
        }
    }

    #[derive(Debug, Serialize)]
    #[serde(untagged)]
    pub enum ListItem {
        Error(String),
        Cert(OutputCert),
    }

    impl ListItem {
        pub fn from_cert_with_config(item: Result<Cert>, config: &Config) -> Self {
            match item {
                Ok(cert) => ListItem::Cert(OutputCert::from_cert_with_config(cert, config)),
                Err(e) => ListItem::Error(format!("{}", e)),
            }
        }
    }

    #[derive(Debug, Serialize)]
    pub struct OutputCert {
        fingerprint: String,
        primary_userid: Option<String>,
        userids: Vec<String>,
    }

    impl OutputCert {
        fn from_cert_with_config(cert: Cert, config: &Config) -> Self {
            // Try to be more helpful by including a User ID in the
            // listing.  We'd like it to be the primary one.  Use
            // decreasingly strict policies.
            let mut primary_uid: Option<Vec<u8>> = None;

            // First, apply our policy.
            if let Ok(vcert) = cert.with_policy(&config.policy, None) {
                if let Ok(primary) = vcert.primary_userid() {
                    primary_uid = Some(primary.value().to_vec());
                }
            }

            // Second, apply the null policy.
            if primary_uid.is_none() {
                let null = openpgp::policy::NullPolicy::new();
                if let Ok(vcert) = cert.with_policy(&null, None) {
                    if let Ok(primary) = vcert.primary_userid() {
                        primary_uid = Some(primary.value().to_vec());
                    }
                }
            }

            // As a last resort, pick the first user id.
            if primary_uid.is_none() {
                if let Some(primary) = cert.userids().next() {
                    primary_uid = Some(primary.value().to_vec());
                }
            }

            // List all user ids independently of their validity.
            let mut userids = vec![];
            for u in cert.userids() {
                if primary_uid.as_ref()
                    .map(|p| &p[..] == u.value()).unwrap_or(false)
                {
                    // Skip the user id we already handled.
                    continue;
                }

                userids.push(Self::userid(u.value()));
            }

            Self {
                fingerprint: format!("{:X}", cert.fingerprint()),
                primary_userid: primary_uid.map(|id| Self::userid(&id)),
                userids,
            }
        }

        fn userid(bytes: &[u8]) -> String {
            String::from_utf8_lossy(bytes).into()
        }
    }
}

// Model output as a data type that can be serialized.
pub mod wkd {
    use super::{OutputVersion, Result, Write};
    use serde::Serialize;

    #[derive(Debug)]
    pub enum WkdUrlVariant {
        Advanced,
        Direct,
    }

    #[derive(Debug, Serialize)]
    pub struct UrlV0 {
        #[serde(skip)]
        variant: WkdUrlVariant,
        sq_output_version: OutputVersion,
        advanced_url: String,
        direct_url: String,
    }

    impl UrlV0 {
        pub const V: OutputVersion = OutputVersion::new(0, 0, 0);

        pub fn new(variant: WkdUrlVariant, advanced_url: String, direct_url: String) -> Self {
            Self {
                sq_output_version: Self::V,
                variant,
                advanced_url,
                direct_url,
            }
        }

        pub fn human_readable(&self, w: &mut dyn Write) -> Result<()> {
            match self.variant {
                WkdUrlVariant::Advanced => writeln!(w, "{}", self.advanced_url)?,
                WkdUrlVariant::Direct => writeln!(w, "{}", self.direct_url)?,
            }
            Ok(())
        }

        pub fn json(&self, w: &mut dyn Write) -> Result<()> {
            super::to_json(w, self)
        }
    }
}
