"""
Hosts file parser for the hosts TUI application.

This module handles reading and writing hosts files, preserving comments
and maintaining file structure integrity.
"""

import os
from pathlib import Path
from .models import HostEntry, HostsFile


class HostsParser:
    """
    Parser for reading and writing hosts files.

    Handles the complete hosts file format including comments,
    blank lines, and both active and inactive entries.
    """

    def __init__(self, file_path: str = "/etc/hosts"):
        """
        Initialize the parser with a hosts file path.

        Args:
            file_path: Path to the hosts file (default: /etc/hosts)
        """
        self.file_path = Path(file_path)

    def parse(self) -> HostsFile:
        """
        Parse the hosts file into a HostsFile object.

        Returns:
            HostsFile object containing all parsed entries and comments

        Raises:
            FileNotFoundError: If the hosts file doesn't exist
            PermissionError: If the file cannot be read
        """
        if not self.file_path.exists():
            raise FileNotFoundError(f"Hosts file not found: {self.file_path}")

        try:
            with open(self.file_path, "r", encoding="utf-8") as f:
                lines = f.readlines()
        except PermissionError:
            raise PermissionError(
                f"Permission denied reading hosts file: {self.file_path}"
            )

        hosts_file = HostsFile()
        entries_started = False

        for line_num, line in enumerate(lines, 1):
            stripped_line = line.strip()

            # Try to parse as a host entry
            entry = HostEntry.from_hosts_line(stripped_line)

            if entry is not None:
                # This is a valid host entry
                hosts_file.entries.append(entry)
                entries_started = True
            elif stripped_line and not entries_started:
                # This is a comment before any entries (header)
                if stripped_line.startswith("#"):
                    comment_text = stripped_line[1:].strip()
                    hosts_file.header_comments.append(comment_text)
                else:
                    # Non-comment, non-entry line before entries
                    hosts_file.header_comments.append(stripped_line)
            elif stripped_line and entries_started:
                # This is a comment after entries have started
                if stripped_line.startswith("#"):
                    comment_text = stripped_line[1:].strip()
                    hosts_file.footer_comments.append(comment_text)
                else:
                    # Non-comment, non-entry line after entries
                    hosts_file.footer_comments.append(stripped_line)
            # Empty lines are ignored but structure is preserved in serialization

        return hosts_file

    def serialize(self, hosts_file: HostsFile) -> str:
        """
        Convert a HostsFile object back to hosts file format with proper column alignment.

        Args:
            hosts_file: HostsFile object to serialize

        Returns:
            String representation of the hosts file with tab-aligned columns
        """
        lines = []

        # Ensure header has management line
        header_comments = self._ensure_management_header(hosts_file.header_comments)

        # Add header comments
        if header_comments:
            for comment in header_comments:
                if comment.strip():
                    lines.append(f"# {comment}")
                else:
                    lines.append("#")

        # Calculate column widths for proper alignment
        ip_width, hostname_width = self._calculate_column_widths(hosts_file.entries)

        # Add host entries with proper column alignment
        for entry in hosts_file.entries:
            lines.append(entry.to_hosts_line(ip_width, hostname_width))

        # Add footer comments
        if hosts_file.footer_comments:
            lines.append("")  # Blank line before footer
            for comment in hosts_file.footer_comments:
                if comment.strip():
                    lines.append(f"# {comment}")
                else:
                    lines.append("#")

        return "\n".join(lines) + "\n"

    def _ensure_management_header(self, header_comments: list) -> list:
        """
        Ensure the header contains the management line with proper formatting.

        Args:
            header_comments: List of existing header comments

        Returns:
            List of header comments with management line added if needed
        """
        management_line = "Managed by hosts - https://git.s1q.dev/phg/hosts"

        # Check if management line already exists
        for comment in header_comments:
            if "git.s1q.dev/phg/hosts" in comment:
                return header_comments

        # If no header exists, create default header
        if not header_comments:
            return ["#", "Host Database", "", management_line, "#"]

        # Check for enclosing comment patterns
        enclosing_pattern = self._detect_enclosing_pattern(header_comments)

        if enclosing_pattern:
            # Insert management line within the enclosing pattern
            return self._insert_in_enclosing_pattern(
                header_comments, management_line, enclosing_pattern
            )
        else:
            # No enclosing pattern, append management line
            result = header_comments.copy()
            result.append(management_line)
            return result

    def _detect_enclosing_pattern(self, header_comments: list) -> dict | None:
        """
        Detect if header has enclosing comment patterns like ###, # #, etc.

        Args:
            header_comments: List of header comments

        Returns:
            Dictionary with pattern info or None if no pattern detected
        """
        if len(header_comments) < 2:
            return None

        # Look for matching patterns at start and end, ignoring management line if present
        first_line = header_comments[0].strip()

        # Find the last line that could be a closing pattern (not the management line)
        last_pattern_index = -1
        for i in range(len(header_comments) - 1, -1, -1):
            line = header_comments[i].strip()
            if "git.s1q.dev/phg/hosts" not in line:
                last_pattern_index = i
                break

        if last_pattern_index <= 0:
            return None

        last_line = header_comments[last_pattern_index].strip()

        # Check for ### pattern
        if first_line == "###" and last_line == "###":
            return {
                "type": "triple_hash",
                "start_index": 0,
                "end_index": last_pattern_index,
                "pattern": "###",
            }

        # Check for # # pattern
        if first_line == "#" and last_line == "#":
            return {
                "type": "single_hash",
                "start_index": 0,
                "end_index": last_pattern_index,
                "pattern": "#",
            }

        # Check for other repeating patterns (like ####, #####, etc.)
        if (
            len(first_line) > 1
            and first_line == last_line
            and all(c == "#" for c in first_line)
        ):
            return {
                "type": "repeating_hash",
                "start_index": 0,
                "end_index": last_pattern_index,
                "pattern": first_line,
            }

        return None

    def _insert_in_enclosing_pattern(
        self, header_comments: list, management_line: str, pattern_info: dict
    ) -> list:
        """
        Insert management line within an enclosing comment pattern.

        Args:
            header_comments: List of header comments
            management_line: Management line to insert
            pattern_info: Information about the enclosing pattern

        Returns:
            Updated list of header comments
        """
        result = header_comments.copy()

        # Find the best insertion point (before the closing pattern)
        insert_index = pattern_info["end_index"]

        # Look for an empty line before the closing pattern to insert after it
        # Otherwise, insert right before the closing pattern
        if insert_index > 1 and header_comments[insert_index - 1].strip() == "":
            # Insert after the empty line, before closing pattern
            result.insert(insert_index, management_line)
        else:
            # Insert empty line and management line before closing pattern
            result.insert(insert_index, "")
            result.insert(insert_index + 1, management_line)

        return result

    def _calculate_column_widths(self, entries: list) -> tuple[int, int]:
        """
        Calculate the maximum width needed for IP and hostname columns.

        Args:
            entries: List of HostEntry objects

        Returns:
            Tuple of (ip_width, hostname_width)
        """
        max_ip_width = 0
        max_hostname_width = 0

        for entry in entries:
            # Calculate IP column width (including comment prefix for inactive entries)
            ip_part = ""
            if not entry.is_active:
                ip_part = "# "
            ip_part += entry.ip_address
            max_ip_width = max(max_ip_width, len(ip_part))

            # Calculate canonical hostname width
            if entry.hostnames:
                canonical_hostname = entry.hostnames[0]
                max_hostname_width = max(max_hostname_width, len(canonical_hostname))

        # Round up to next tab stop (8-character boundaries) for better alignment
        tab_width = 8
        ip_width = ((max_ip_width + tab_width - 1) // tab_width) * tab_width
        hostname_width = ((max_hostname_width + tab_width - 1) // tab_width) * tab_width

        return ip_width, hostname_width

    def write(self, hosts_file: HostsFile, backup: bool = True) -> None:
        """
        Write a HostsFile object to the hosts file.

        Args:
            hosts_file: HostsFile object to write
            backup: Whether to create a backup before writing

        Raises:
            PermissionError: If the file cannot be written
            OSError: If there's an error during file operations
        """
        # Create backup if requested
        if backup and self.file_path.exists():
            backup_path = self.file_path.with_suffix(".bak")
            try:
                import shutil

                shutil.copy2(self.file_path, backup_path)
            except Exception as e:
                raise OSError(f"Failed to create backup: {e}")

        # Serialize the hosts file
        content = self.serialize(hosts_file)

        # Write atomically using a temporary file
        temp_path = self.file_path.with_suffix(".tmp")
        try:
            with open(temp_path, "w", encoding="utf-8") as f:
                f.write(content)

            # Atomic move
            temp_path.replace(self.file_path)

        except Exception as e:
            # Clean up temp file if it exists
            if temp_path.exists():
                temp_path.unlink()
            raise OSError(f"Failed to write hosts file: {e}")

    def validate_write_permissions(self) -> bool:
        """
        Check if we have write permissions to the hosts file.

        Returns:
            True if we can write to the file, False otherwise
        """
        try:
            # Check if file exists and is writable
            if self.file_path.exists():
                return os.access(self.file_path, os.W_OK)
            else:
                # Check if parent directory is writable
                return os.access(self.file_path.parent, os.W_OK)
        except Exception:
            return False

    def get_file_info(self) -> dict:
        """
        Get information about the hosts file.

        Returns:
            Dictionary with file information
        """
        info = {
            "path": str(self.file_path),
            "exists": self.file_path.exists(),
            "readable": False,
            "writable": False,
            "size": 0,
            "modified": None,
        }

        if info["exists"]:
            try:
                info["readable"] = os.access(self.file_path, os.R_OK)
                info["writable"] = os.access(self.file_path, os.W_OK)
                stat = self.file_path.stat()
                info["size"] = stat.st_size
                info["modified"] = stat.st_mtime
            except Exception:
                pass

        return info


class HostsParserError(Exception):
    """Base exception for hosts parser errors."""

    pass


class HostsFileNotFoundError(HostsParserError):
    """Raised when the hosts file is not found."""

    pass


class HostsPermissionError(HostsParserError):
    """Raised when there are permission issues with the hosts file."""

    pass


class HostsValidationError(HostsParserError):
    """Raised when hosts file content is invalid."""

    pass
