use std::{
    collections::BTreeMap,
    fmt,
    fs::OpenOptions,
    io::{
        self,
        BufRead,
        BufReader,
        Write,
    },
    net::IpAddr,
    path::{
        Path,
        PathBuf,
    },
    time::{
        SystemTime,
        UNIX_EPOCH,
    },
};

pub type Result<T> = std::result::Result<T, HostsFileError>;

#[derive(Debug, Clone)]
pub enum HostsFileError {
    Io(String),
    InvalidPath(String),
    InvalidData(String),
    UnsupportedPlatform,
}

impl fmt::Display for HostsFileError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Io(msg) => write!(f, "IO error: {}", msg),
            Self::InvalidPath(msg) => write!(f, "Invalid path: {}", msg),
            Self::InvalidData(msg) => write!(f, "Invalid data: {}", msg),
            Self::UnsupportedPlatform => write!(f, "Unsupported platform"),
        }
    }
}

impl std::error::Error for HostsFileError {}

impl From<io::Error> for HostsFileError {
    fn from(err: io::Error) -> Self {
        Self::Io(err.to_string())
    }
}

pub struct HostsFile {
    entries: BTreeMap<IpAddr, Vec<String>>,
    tag: String,
}

impl HostsFile {
    pub fn new<S: Into<String>>(tag: S) -> Self {
        Self {
            entries: BTreeMap::new(),
            tag: tag.into(),
        }
    }

    pub fn add_entry<S: ToString>(&mut self, ip: IpAddr, hostname: S) -> &mut Self {
        self.entries
            .entry(ip)
            .or_default()
            .push(hostname.to_string());
        self
    }

    pub fn add_entries<I, S>(&mut self, ip: IpAddr, hostnames: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: ToString,
    {
        self.entries
            .entry(ip)
            .or_default()
            .extend(hostnames.into_iter().map(|h| h.to_string()));
        self
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn entry_count(&self) -> usize {
        self.entries.len()
    }

    pub fn write(&self) -> Result<bool> {
        self.write_to(get_default_hosts_path()?)
    }

    pub fn write_to<P: AsRef<Path>>(&self, path: P) -> Result<bool> {
        let path = path.as_ref();
        validate_hosts_path(path)?;

        let writer = HostsFileWriter::new(path);
        writer.update_section(&self.tag, &self.entries)
    }
}

struct HostsSection {
    tag: String,
}

impl HostsSection {
    fn new(tag: &str) -> Self {
        Self {
            tag: tag.to_string(),
        }
    }

    fn begin_marker(&self) -> String {
        format!("# DO NOT EDIT {} BEGIN", self.tag)
    }

    fn end_marker(&self) -> String {
        format!("# DO NOT EDIT {} END", self.tag)
    }

    fn format_entries(&self, entries: &BTreeMap<IpAddr, Vec<String>>) -> Vec<String> {
        if entries.is_empty() {
            return vec![];
        }

        let mut lines = vec![self.begin_marker()];

        for (ip, hostnames) in entries {
            lines.extend(self.format_host_entries(ip, hostnames));
        }

        lines.push(self.end_marker());
        lines
    }

    fn format_host_entries(&self, ip: &IpAddr, hostnames: &[String]) -> Vec<String> {
        if cfg!(windows) {
            hostnames
                .iter()
                .map(|hostname| format!("{} {}", ip, hostname))
                .collect()
        } else {
            vec![format!("{} {}", ip, hostnames.join(" "))]
        }
    }

    fn find_section_bounds(&self, lines: &[String]) -> SectionBounds {
        let begin_marker = self.begin_marker();
        let end_marker = self.end_marker();

        let begin = lines.iter().position(|line| line.trim() == begin_marker);
        let end = lines.iter().position(|line| line.trim() == end_marker);

        SectionBounds { begin, end }
    }
}

#[derive(Debug)]
struct SectionBounds {
    begin: Option<usize>,
    end: Option<usize>,
}

impl SectionBounds {
    fn is_complete(&self) -> bool {
        self.begin.is_some() && self.end.is_some()
    }

    fn is_missing(&self) -> bool {
        self.begin.is_none() && self.end.is_none()
    }

    fn is_partial(&self) -> bool {
        !self.is_complete() && !self.is_missing()
    }
}

struct HostsFileWriter<'a> {
    path: &'a Path,
}

impl<'a> HostsFileWriter<'a> {
    fn new(path: &'a Path) -> Self {
        Self { path }
    }

    fn update_section(&self, tag: &str, entries: &BTreeMap<IpAddr, Vec<String>>) -> Result<bool> {
        let mut lines = self.read_file_lines()?;
        let section = HostsSection::new(tag);
        let new_section_lines = section.format_entries(entries);

        let changed = self.apply_section_update(&mut lines, &section, new_section_lines)?;

        if changed {
            self.write_file_lines(&lines)?;
        }

        Ok(changed)
    }

    fn read_file_lines(&self) -> Result<Vec<String>> {
        let file = OpenOptions::new()
            .create(true)
            .read(true)
            .write(true)
            .truncate(false)
            .open(self.path)?;

        Ok(BufReader::new(file)
            .lines()
            .collect::<io::Result<Vec<_>>>()?)
    }

    fn apply_section_update(
        &self, lines: &mut Vec<String>, section: &HostsSection, new_section_lines: Vec<String>,
    ) -> Result<bool> {
        let bounds = section.find_section_bounds(lines);

        if bounds.is_partial() {
            return Err(HostsFileError::InvalidData(format!(
                "Incomplete section markers for tag '{}'",
                section.tag
            )));
        }

        if bounds.is_complete() {
            self.replace_existing_section(lines, &bounds, new_section_lines)
        } else {
            self.add_new_section(lines, new_section_lines)
        }
    }

    fn replace_existing_section(
        &self, lines: &mut Vec<String>, bounds: &SectionBounds, new_section_lines: Vec<String>,
    ) -> Result<bool> {
        let begin = bounds.begin.unwrap();
        let end = bounds.end.unwrap();

        let old_section: Vec<String> = lines.drain(begin..=end).collect();

        if old_section == new_section_lines {
            lines.splice(begin..begin, old_section);
            return Ok(false);
        }

        lines.splice(begin..begin, new_section_lines);
        Ok(true)
    }

    fn add_new_section(
        &self, lines: &mut Vec<String>, new_section_lines: Vec<String>,
    ) -> Result<bool> {
        if new_section_lines.is_empty() {
            return Ok(false);
        }

        if let Some(last_line) = lines.last()
            && !last_line.is_empty()
        {
            lines.push(String::new());
        }

        lines.extend(new_section_lines);
        Ok(true)
    }

    fn write_file_lines(&self, lines: &[String]) -> Result<()> {
        let content = self.format_file_content(lines)?;
        let writer = AtomicFileWriter::new(self.path);
        writer.write_content(&content)
    }

    fn format_file_content(&self, lines: &[String]) -> Result<Vec<u8>> {
        let mut buffer = Vec::new();
        for line in lines {
            writeln!(buffer, "{}", line)?;
        }
        Ok(buffer)
    }
}

struct AtomicFileWriter<'a> {
    target_path: &'a Path,
}

impl<'a> AtomicFileWriter<'a> {
    fn new(path: &'a Path) -> Self {
        Self { target_path: path }
    }

    fn write_content(&self, content: &[u8]) -> Result<()> {
        match self.try_atomic_write(content) {
            Ok(()) => {
                log::debug!("Successfully wrote hosts file using atomic write");
                Ok(())
            }
            Err(_) => {
                log::debug!("Atomic write failed, falling back to direct write");
                self.write_directly(content)
            }
        }
    }

    fn try_atomic_write(&self, content: &[u8]) -> Result<()> {
        let temp_path = self.create_temp_path()?;

        std::fs::copy(self.target_path, &temp_path)?;

        #[cfg(target_os = "linux")]
        self.preserve_selinux_context(&temp_path);

        self.write_file(&temp_path, content)?;
        std::fs::rename(&temp_path, self.target_path)?;

        Ok(())
    }

    fn create_temp_path(&self) -> Result<PathBuf> {
        let parent = self.target_path.parent().ok_or_else(|| {
            HostsFileError::InvalidPath("Path has no parent directory".to_string())
        })?;

        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("System time is before Unix epoch")
            .as_millis();

        let filename = self
            .target_path
            .file_name()
            .ok_or_else(|| HostsFileError::InvalidPath("Path has no filename".to_string()))?;

        let temp_filename = format!("{}.tmp{}", filename.to_string_lossy(), timestamp);
        Ok(parent.join(temp_filename))
    }

    #[cfg(target_os = "linux")]
    fn preserve_selinux_context(&self, _temp_path: &Path) {
        log::trace!("SELinux context preservation not implemented");
    }

    fn write_directly(&self, content: &[u8]) -> Result<()> {
        self.write_file(self.target_path, content)
    }

    fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
        OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(path)?
            .write_all(content)?;
        Ok(())
    }
}

fn get_default_hosts_path() -> Result<PathBuf> {
    let path = get_platform_hosts_path()?;

    if !path.exists() {
        return Err(HostsFileError::InvalidPath(format!(
            "Hosts file not found at {}",
            path.display()
        )));
    }

    Ok(path)
}

fn get_platform_hosts_path() -> Result<PathBuf> {
    if cfg!(unix) {
        Ok(PathBuf::from("/etc/hosts"))
    } else if cfg!(windows) {
        let windir = std::env::var("WinDir").map_err(|_| {
            HostsFileError::InvalidPath("WinDir environment variable not found".to_string())
        })?;
        Ok(PathBuf::from(format!(
            "{}\\System32\\Drivers\\Etc\\hosts",
            windir
        )))
    } else {
        Err(HostsFileError::UnsupportedPlatform)
    }
}

fn validate_hosts_path(path: &Path) -> Result<()> {
    if path.is_dir() {
        Err(HostsFileError::InvalidPath(
            "Expected file path, got directory".to_string(),
        ))
    } else {
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::io::Write;

    use super::*;

    #[test]
    fn test_hosts_file_write() {
        let (mut temp_file, temp_path) = tempfile::NamedTempFile::new().unwrap().into_parts();
        temp_file.write_all(b"preexisting\ncontent").unwrap();

        let mut hosts_file = HostsFile::new("test");
        hosts_file.add_entry([1, 1, 1, 1].into(), "example.com");

        assert!(hosts_file.write_to(&temp_path).unwrap());
        assert!(!hosts_file.write_to(&temp_path).unwrap());

        let contents = std::fs::read_to_string(&temp_path).unwrap();
        assert!(contents.contains("preexisting\ncontent"));
        assert!(contents.contains("# DO NOT EDIT test BEGIN"));
        assert!(contents.contains("1.1.1.1 example.com"));
        assert!(contents.contains("# DO NOT EDIT test END"));
    }

    #[test]
    fn test_fluent_api() {
        let mut hosts_file = HostsFile::new("test");
        hosts_file
            .add_entry([127, 0, 0, 1].into(), "localhost")
            .add_entries([192, 168, 1, 1].into(), ["router", "gateway"]);

        assert_eq!(hosts_file.entries.len(), 2);
    }
}
