use log::trace;
use std::{
    fmt,
    fmt::Debug,
    fs::File,
    io::{BufRead, BufReader},
    net::IpAddr,
    path::PathBuf,
};

#[derive(Debug, Eq, PartialEq, Clone)]
pub struct HostsFile {
    pub path: PathBuf,
    pub fields: Vec<HostsFieldType>,
}

pub type HostsComment = String;

#[derive(Debug, Eq, PartialEq, Clone)]
pub enum HostsFieldType {
    Address(HostsAddress),
    Comment(HostsComment),
}

#[derive(Debug, Eq, PartialEq, Clone)]
pub struct HostsAddress {
    pub ip: Option<IpAddr>,
    pub hosts: Vec<String>,
    pub comment: Option<HostsComment>,
}

impl HostsFile {
    #[must_use]
    pub fn load() -> Self {
        // TODO: Auto-find path or allow user defined path
        let path: PathBuf = "/etc/hosts".parse().unwrap();
        let mut fields: Vec<HostsFieldType> = Vec::new();

        let file = File::open(path.clone());
        let reader = BufReader::new(file.unwrap());
        for line in reader.lines() {
            let clean_line = line.unwrap().trim().to_string();

            // Empty line
            if clean_line.is_empty() {
                continue;
            }

            // Comment field
            // TODO: Support multi-string comments better?
            // TODO: If starts with '#', grab the next X lines which also start with '#'?
            if clean_line.starts_with('#') {
                let comment_field = HostsFieldType::Comment(clean_line);
                fields.push(comment_field);
                continue;
            }

            // Address field
            let address = HostsAddress::new(&clean_line);
            fields.push(HostsFieldType::Address(address));
        }

        Self { path, fields }
    }

    #[must_use]
    pub fn field_exists(&self, search_field: &str) -> bool {
        let mut found = false;

        for field in &self.fields {
            match field {
                HostsFieldType::Comment(_) => continue,
                HostsFieldType::Address(field) => {
                    if field.ip.is_some() && field.ip.unwrap().to_string() == search_field {
                        found = true;
                        println!("{}", field)
                    }

                    for host in &field.hosts {
                        if *host == search_field {
                            found = true;
                            println!("{}", field)
                        }
                    }
                }
            }
        }

        found
    }
}

impl fmt::Display for HostsFile {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        trace!("Hosts Path: {}", self.path.to_str().unwrap().trim());

        for field in &self.fields {
            match field {
                HostsFieldType::Comment(field) => writeln!(formatter, "{}", &field)?,
                HostsFieldType::Address(field) => writeln!(formatter, "{}", &field)?,
            }
        }

        Ok(())
    }
}

impl HostsAddress {
    #[must_use]
    pub fn new(input: &str) -> Self {
        let mut ip: Option<IpAddr> = None;
        let mut hosts: Vec<String> = Vec::new();
        let mut comment: Option<String> = None;

        let line_parts = input.split_whitespace();
        for (index, line_part) in line_parts.clone().enumerate() {
            let trimmed_line_part = line_part.trim().to_string();

            // First part should be IP
            if index == 0 {
                ip = Some(trimmed_line_part.parse().expect("Failed to parse to IP address"));
                continue;
            }

            // Remaining text after initial '#' is all comment
            // TODO: Implement smarter comment parsing to identify 'tags'?
            if line_part.starts_with('#') {
                let comment_parts: Vec<&str> = line_parts.skip(index).collect();
                comment = Some(comment_parts.join(" ").trim().to_string());
                break;
            }

            hosts.push(trimmed_line_part);
        }

        Self { ip, hosts, comment }
    }
}

impl fmt::Display for HostsAddress {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        if self.comment.is_some() {
            writeln!(formatter, "{}", self.comment.as_ref().unwrap())?;
        }

        for host in &self.hosts {
            write!(formatter, "{} {}", self.ip.unwrap().to_string(), &host)?
        }

        Ok(())
    }
}
