use syn::parse::{Parse, ParseStream, Result};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{Ident, LitInt, LitStr};

use crate::util::{parse_eq, unknown_attribute, POSTGRES_TYPE_NOTE, POSTGRES_TYPE_NOTE_ID};

enum Attr {
    Oid(Ident, LitInt),
    ArrayOid(Ident, LitInt),
    Name(Ident, LitStr),
    Schema(Ident, LitStr),
}

impl Parse for Attr {
    fn parse(input: ParseStream) -> Result<Self> {
        let name: Ident = input.parse()?;
        let name_str = name.to_string();

        match &*name_str {
            "oid" => Ok(Attr::Oid(name, parse_eq(input, POSTGRES_TYPE_NOTE_ID)?)),
            "array_oid" => Ok(Attr::ArrayOid(
                name,
                parse_eq(input, POSTGRES_TYPE_NOTE_ID)?,
            )),
            "name" => Ok(Attr::Name(name, parse_eq(input, POSTGRES_TYPE_NOTE)?)),
            "schema" => Ok(Attr::Schema(name, parse_eq(input, POSTGRES_TYPE_NOTE)?)),

            _ => Err(unknown_attribute(
                &name,
                &["oid", "array_oid", "name", "schema"],
            )),
        }
    }
}

pub enum PostgresType {
    Fixed(LitInt, LitInt),
    Lookup(LitStr, Option<LitStr>),
}

impl Parse for PostgresType {
    fn parse(input: ParseStream) -> Result<Self> {
        let mut oid = None;
        let mut array_oid = None;
        let mut name = None;
        let mut schema = None;

        for attr in Punctuated::<Attr, Comma>::parse_terminated(input)? {
            match attr {
                Attr::Oid(ident, value) => oid = Some((ident, value)),
                Attr::ArrayOid(ident, value) => array_oid = Some((ident, value)),
                Attr::Name(ident, value) => name = Some((ident, value)),
                Attr::Schema(ident, value) => schema = Some((ident, value)),
            }
        }

        Self::validate_and_build(input, oid, array_oid, name, schema)
    }
}

impl PostgresType {
    pub fn validate_and_build(
        input: ParseStream,
        oid: Option<(Ident, LitInt)>,
        array_oid: Option<(Ident, LitInt)>,
        name: Option<(Ident, LitStr)>,
        schema: Option<(Ident, LitStr)>,
    ) -> Result<Self> {
        let help = format!(
            "the correct format looks like either `#[diesel({POSTGRES_TYPE_NOTE})]` or `#[diesel({POSTGRES_TYPE_NOTE_ID})]`"
        );

        if let Some((_, name)) = name {
            if let Some((oid, _)) = oid {
                Err(syn::Error::new(
                    oid.span(),
                    format!("unexpected `oid` when `name` is present\nhelp: {help}"),
                ))
            } else if let Some((array_oid, _)) = array_oid {
                Err(syn::Error::new(
                    array_oid.span(),
                    format!("unexpected `array_oid` when `name` is present\nhelp: {help}"),
                ))
            } else {
                Ok(PostgresType::Lookup(name, schema.map(|s| s.1)))
            }
        } else if let Some((schema, lit)) = schema {
            Err(syn::Error::new(
                schema.span(),
                format!(
                    "expected `name` to be also present\n\
                     help: make sure `name` is present, `#[diesel(postgres_type(name = \"...\", schema = \"{}\"))]`", lit.value()
                ),
            ))
        } else if let (Some((_, oid)), Some((_, array_oid))) = (oid, array_oid) {
            Ok(PostgresType::Fixed(oid, array_oid))
        } else {
            Err(syn::Error::new(
                input.span(),
                format!(
                    "expected `oid` and `array_oid` attribute or `name` attribute\nhelp: {help}"
                ),
            ))
        }
    }
}
