use std::fs::OpenOptions;
use std::io::BufWriter;
use std::{
    fmt::Debug,
    fs::File,
    io,
    io::{BufRead, BufReader, Write},
    net::IpAddr,
    path::PathBuf,
};

use tabwriter::TabWriter;

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct HostsFile {
    pub path: PathBuf,
    pub fields: Vec<HostsAddress>,
    pub is_dirty: bool,
}

impl HostsFile {
    #[must_use]
    pub fn load() -> Self {
        let path: PathBuf = "/etc/hosts".parse().unwrap();

        let mut is_dirty: bool = false;
        let mut fields: Vec<HostsAddress> = 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 - ignore
            if clean_line.is_empty() {
                is_dirty = true;
                continue;
            }

            // Comment - ignore
            if clean_line.starts_with('#') {
                is_dirty = true;
                continue;
            }

            // Hosts field
            let addresses = HostsEntry::new(&clean_line);
            for (index, host) in addresses.hosts.iter().enumerate() {
                if index > 0 {
                    is_dirty = true;
                }

                fields.push(HostsAddress {
                    ip: addresses.ip,
                    host: host.clone(),
                })
            }
        }

        let fields_before = fields.len();
        fields.sort();
        fields.dedup();

        /// TODO: Check for duplicated hostnames usage
        /// ```
        /// 127.0.0.1    hello.test
        /// 0.0.0.0      hello.test
        /// ```
        let fields_after = fields.len();
        if fields_before != fields_after {
            is_dirty = true;
        }

        Self {
            path,
            fields,
            is_dirty,
        }
    }

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

        for field in &self.fields {
            if field.ip.to_string() == search_field {
                found = true;
                println!("{} {}", field.ip, field.host)
            }

            if field.host == search_field {
                found = true;
                println!("{} {}", field.ip, field.host)
            }
        }

        found
    }

    pub fn add_field(&mut self, ip: &str, hostname: &str) -> std::io::Result<()> {
        let parsed_ip: IpAddr = ip.parse().expect("Failed to parse to IP address");

        self.fields.push(HostsAddress {
            ip: parsed_ip,
            host: hostname.parse().unwrap(),
        });

        self.order_fields();
        self.write()?;

        Ok(())
    }

    pub fn delete_field(&mut self) -> std::io::Result<()> {
        Ok(())
    }

    pub fn write(&mut self) -> std::io::Result<()> {
        let file = OpenOptions::new()
            .write(true)
            .truncate(true)
            .create(true)
            .open(&self.path)?;

        let writer = BufWriter::new(&file);
        let mut tab_writer = TabWriter::new(writer);

        for field in &self.fields {
            writeln!(&mut tab_writer, "{}\t{}", &field.ip, &field.host)?;
        }

        tab_writer.flush()?;
        Ok(())
    }

    pub fn print(&self) {
        let mut stdout = TabWriter::new(io::stdout());

        for field in &self.fields {
            writeln!(&mut stdout, "{}\t{}", &field.ip, &field.host).unwrap();
        }

        stdout.flush().unwrap();
    }

    fn order_fields(&mut self) {
        self.fields.sort();
        self.fields.dedup();
    }
}

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct HostsEntry {
    pub ip: IpAddr,
    pub hosts: Vec<String>,
}

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

        let line_parts = input.split_whitespace();
        for (index, line_part) in line_parts.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 '#' is a comment
            if line_part.starts_with('#') {
                break;
            }

            hosts.push(trimmed_line_part);
        }

        if ip.is_none() {
            panic!("Invalid hosts entry: {}", input);
        }

        hosts.sort();
        hosts.dedup();

        Self {
            ip: ip.unwrap(),
            hosts,
        }
    }
}

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct HostsAddress {
    pub ip: IpAddr,
    pub host: String,
}
