use std::str::FromStr;
use std::string::ToString;
use std::{error::Error, fmt};

#[derive(Debug)]
pub struct BarbParseError {}

impl Error for BarbParseError {}

impl fmt::Display for BarbParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Error parsing barb file")
    }
}

pub enum Method {
    GET,
    PUT,
    POST,
    PATCH,
    DELETE,
}

impl FromStr for Method {
    type Err = BarbParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "GET" => Ok(Self::GET),
            "PUT" => Ok(Self::PUT),
            "POST" => Ok(Self::POST),
            "PATCH" => Ok(Self::PATCH),
            "DELETE" => Ok(Self::DELETE),
            _ => Err(BarbParseError {}),
        }
    }
}

impl ToString for Method {
    fn to_string(&self) -> String {
        match self {
            Self::GET => String::from("GET"),
            Self::PUT => String::from("PUT"),
            Self::POST => String::from("POST"),
            Self::PATCH => String::from("PATCH"),
            Self::DELETE => String::from("DELETE"),
        }
    }
}

impl Method {
    pub fn takes_body(&self) -> bool {
        match self {
            Method::GET => false,
            Method::DELETE => false,
            _ => true
        }
    }
}

#[derive(Debug)]
struct Header {
    name: String,
    value: String,
}

impl fmt::Display for Header {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.name, self.value)
    }
}

struct BarbHeader {
    pub method: Method,
    pub url: String,
    pub headers: Vec<Header>,
    pub filter: Option<String>,
}

impl BarbHeader {
    fn new(method: Method, url: String, headers: Vec<Header>, filter: Option<String>) -> Self {
        BarbHeader {
            method: method,
            url: url,
            headers: headers,
            filter: filter,
        }
    }
}

pub struct BarbFile {
    header: BarbHeader,
    body: Option<String>,
}

impl BarbFile {
    pub fn headers(&self) -> &Vec<Header> {
        &self.header.headers
    }

    pub fn method(&self) -> &Method {
        &self.header.method
    }

    pub fn method_as_string(&self) -> String {
        self.header.method.to_string()
    }

    pub fn url(&self) -> &String {
        &self.header.url
    }

    pub fn body(&self) -> &Option<String> {
        &self.body
    }
}

fn decode_url_line(line: &str) -> Result<(Method, String), BarbParseError> {
    let mut components = line[1..].split('^');
    let meth = components.next().ok_or(BarbParseError {})?;
    let url = components.next().ok_or(BarbParseError {})?;
    return Ok((Method::from_str(meth)?, String::from(url)));
}

fn decode_header(line: &str) -> Result<Header, BarbParseError> {
    let mut components = line[1..].split(':');
    let header_name = components.next().ok_or(BarbParseError {})?;
    let header_val = components.next().ok_or(BarbParseError {})?.trim();
    Ok(Header {
        name: String::from(header_name),
        value: String::from(header_val),
    })
}

impl FromStr for BarbFile {
    type Err = BarbParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut lines = s.split('\n');
        let (method, url) = decode_url_line(lines.next().ok_or(BarbParseError {})?)?;
        let mut headers: Vec<Header> = vec![];
        let mut filter = None;

        for line in &mut lines {
            if line == "" {
                // End of header.
                break;
            }

            if let Some(_) = line.find(':') {
                headers.push(decode_header(line).unwrap());
            }

            if let None = filter {
                filter = match &line[0..2] {
                    "#|" => Some(String::from(&line[2..])),
                    _ => None,
                }
            }
        }

        let body = lines.fold(String::from(""), |acc, x| acc + x);

        Ok(BarbFile {
            header: BarbHeader::new(method, url, headers, filter),
            body: if body == "" {None} else {Some(body)},
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_method_from_str() {
        assert!(matches!(Method::from_str("GET").unwrap(), Method::GET));
        assert!(matches!(Method::from_str("GET").unwrap(), Method::GET));
        assert!(matches!(Method::from_str("PUT").unwrap(), Method::PUT));
        assert!(matches!(Method::from_str("POST").unwrap(), Method::POST));
        assert!(matches!(Method::from_str("PATCH").unwrap(), Method::PATCH));
        assert!(matches!(
            Method::from_str("DELETE").unwrap(),
            Method::DELETE
        ));
    }

    #[test]
    fn test_decode_url_line() {
        let (method, url) = decode_url_line("#GET^http://blahblah").unwrap();
        assert!(matches!(method, Method::GET));
        assert_eq!(url, "http://blahblah");
    }

    #[test]
    fn test_decode_header() {
        let hdr = decode_header("#Authorization: TOKEN 12345").unwrap();
        assert_eq!(hdr.name, "Authorization");
        assert_eq!(hdr.value, "TOKEN 12345");
    }

    #[test]
    fn test_parse_barbfile_no_body() {
        let barbfile =
            BarbFile::from_str("#GET^https://blah.com/api/blah\n#Authorization: BLAH\n#|filtr\n")
                .unwrap();
        assert!(matches!(barbfile.header.method, Method::GET));
        assert_eq!(barbfile.header.url, "https://blah.com/api/blah");
        assert_eq!(barbfile.header.filter, Some(String::from("filtr")));
        assert_eq!(barbfile.header.headers.len(), 1);
        assert_eq!(barbfile.header.headers[0].name, "Authorization");
        assert_eq!(barbfile.header.headers[0].value, "BLAH");
        assert_eq!(barbfile.body, None);
    }

    #[test]
    fn test_parse_barbfile_body() {
        let barbfile =
            BarbFile::from_str("#POST^https://blah.com/api/blah\n#Authorization: BLAH\n#|filtr\n\n{\"key\":\"value\"}\n")
                .unwrap();
        assert!(matches!(barbfile.header.method, Method::POST));
        assert_eq!(barbfile.header.url, "https://blah.com/api/blah");
        assert_eq!(barbfile.header.filter, Some(String::from("filtr")));
        assert_eq!(barbfile.header.headers.len(), 1);
        assert_eq!(barbfile.header.headers[0].name, "Authorization");
        assert_eq!(barbfile.header.headers[0].value, "BLAH");
        assert_eq!(barbfile.body, Some(String::from("{\"key\":\"value\"}")))
    }
}