"""
Data models for the hosts TUI application.

This module defines the core data structures used throughout the application
for representing hosts file entries and the overall hosts file structure.
"""

from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime
import ipaddress
import re


@dataclass
class HostEntry:
    """
    Represents a single entry in the hosts file.

    Attributes:
        ip_address: The IP address (IPv4 or IPv6)
        hostnames: List of hostnames mapped to this IP
        comment: Optional comment for this entry
        is_active: Whether this entry is active (not commented out)
        dns_name: Optional DNS name for CNAME-like functionality
        resolved_ip: Currently resolved IP address from DNS
        last_resolved: Timestamp of last DNS resolution
        dns_resolution_status: Current DNS resolution status
    """

    ip_address: str
    hostnames: List[str]
    comment: Optional[str] = None
    is_active: bool = True
    dns_name: Optional[str] = None
    resolved_ip: Optional[str] = None
    last_resolved: Optional[datetime] = None
    dns_resolution_status: Optional[str] = None

    def __post_init__(self):
        """Validate the entry after initialization."""
        self.validate()

    def is_default_entry(self) -> bool:
        """
        Check if this entry is a system default entry.

        Returns:
            True if this is a default system entry (localhost, broadcasthost, ::1)
        """
        if not self.hostnames:
            return False

        canonical_hostname = self.hostnames[0]
        default_entries = [
            {"ip": "127.0.0.1", "hostname": "localhost"},
            {"ip": "255.255.255.255", "hostname": "broadcasthost"},
            {"ip": "::1", "hostname": "localhost"},
        ]

        for entry in default_entries:
            if (
                entry["ip"] == self.ip_address
                and entry["hostname"] == canonical_hostname
            ):
                return True
        return False

    def has_dns_name(self) -> bool:
        """Check if this entry has a DNS name configured."""
        return self.dns_name is not None and self.dns_name.strip() != ""

    def needs_dns_resolution(self) -> bool:
        """Check if this entry needs DNS resolution."""
        return self.has_dns_name() and self.dns_resolution_status != "resolved"

    def is_dns_resolution_stale(self, max_age_seconds: int = 300) -> bool:
        """Check if DNS resolution is stale and needs refresh."""
        if not self.last_resolved:
            return True
        age = (datetime.now() - self.last_resolved).total_seconds()
        return age > max_age_seconds

    def get_display_ip(self) -> str:
        """Get the IP address to display (resolved IP if available, otherwise stored IP)."""
        if self.has_dns_name() and self.resolved_ip:
            return self.resolved_ip
        return self.ip_address

    def validate(self) -> None:
        """
        Validate the host entry data.

        Raises:
            ValueError: If the IP address or hostnames are invalid
        """
        # Validate IP address (allow empty IP for DNS-only entries)
        if self.ip_address:
            try:
                ipaddress.ip_address(self.ip_address)
            except ValueError as e:
                raise ValueError(f"Invalid IP address '{self.ip_address}': {e}")
        elif not self.has_dns_name():
            # If no IP address, must have a DNS name
            raise ValueError("Entry must have either an IP address or a DNS name")

        # Validate hostnames
        if not self.hostnames:
            raise ValueError("At least one hostname is required")

        hostname_pattern = re.compile(
            r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$"
        )

        for hostname in self.hostnames:
            if not hostname_pattern.match(hostname):
                raise ValueError(f"Invalid hostname '{hostname}'")

        # Validate DNS name if present
        if self.dns_name:
            if not hostname_pattern.match(self.dns_name):
                raise ValueError(f"Invalid DNS name '{self.dns_name}'")

        # Validate resolved IP if present
        if self.resolved_ip:
            try:
                ipaddress.ip_address(self.resolved_ip)
            except ValueError as e:
                raise ValueError(f"Invalid resolved IP address '{self.resolved_ip}': {e}")

    def to_hosts_line(self, ip_width: int = 0, hostname_width: int = 0) -> str:
        """
        Convert this entry to a hosts file line with proper tab alignment.

        Args:
            ip_width: Width of the IP address column for alignment
            hostname_width: Width of the canonical hostname column for alignment

        Returns:
            String representation suitable for writing to hosts file
        """
        line_parts = []

        # Build the IP address part (with comment prefix if inactive)
        ip_part = ""
        if not self.is_active:
            ip_part = "# "
        ip_part += self.ip_address

        # Calculate tabs needed for IP column alignment
        ip_tabs = self._calculate_tabs_needed(len(ip_part), ip_width)

        # Build the canonical hostname part
        canonical_hostname = self.hostnames[0] if self.hostnames else ""
        hostname_tabs = self._calculate_tabs_needed(
            len(canonical_hostname), hostname_width
        )

        # Start building the line
        line_parts.append(ip_part)
        line_parts.append("\t" * max(1, ip_tabs))  # At least one tab
        line_parts.append(canonical_hostname)

        # Add additional hostnames (aliases) with single tab separation
        if len(self.hostnames) > 1:
            line_parts.append("\t" * max(1, hostname_tabs))
            line_parts.append("\t".join(self.hostnames[1:]))

        # Build comment section (DNS metadata + user comment)
        comment_parts = []
        
        # Add DNS metadata if present
        if self.has_dns_name():
            dns_meta = f"DNS:{self.dns_name}"
            if self.dns_resolution_status:
                dns_meta += f"|Status:{self.dns_resolution_status}"
            if self.last_resolved:
                dns_meta += f"|Last:{self.last_resolved.isoformat()}"
            comment_parts.append(dns_meta)
        
        # Add user comment if present
        if self.comment:
            comment_parts.append(self.comment)
        
        # Add complete comment section
        if comment_parts:
            if len(self.hostnames) <= 1:
                line_parts.append("\t" * max(1, hostname_tabs))
            else:
                line_parts.append("\t")
            line_parts.append(f"# {' | '.join(comment_parts)}")

        return "".join(line_parts)

    def _calculate_tabs_needed(self, current_length: int, target_width: int) -> int:
        """
        Calculate number of tabs needed to reach target column width.

        Args:
            current_length: Current string length
            target_width: Target column width

        Returns:
            Number of tabs needed (minimum 1)
        """
        if target_width <= current_length:
            return 1

        # Calculate tabs needed (assuming tab width of 8)
        tab_width = 8
        remaining_space = target_width - current_length
        tabs_needed = (remaining_space + tab_width - 1) // tab_width  # Ceiling division
        return max(1, tabs_needed)

    @classmethod
    def from_hosts_line(cls, line: str) -> Optional["HostEntry"]:
        """
        Parse a hosts file line into a HostEntry.

        Args:
            line: A line from the hosts file

        Returns:
            HostEntry instance or None if line is empty/comment-only
        """
        original_line = line.strip()
        if not original_line:
            return None

        # Check if line is commented out (inactive)
        is_active = True
        if original_line.startswith("#"):
            is_active = False
            line = original_line[1:].strip()

        # Handle comment-only lines
        if not line or line.startswith("#"):
            return None

        # Split line into parts, handling both spaces and tabs
        import re

        # Split on any whitespace (spaces, tabs, or combinations)
        parts = re.split(r"\s+", line.strip())
        if len(parts) < 2:
            return None

        ip_address = parts[0]
        hostnames = []
        comment = None

        # Parse hostnames and comments
        for i, part in enumerate(parts[1:], 1):
            if part.startswith("#"):
                # Everything from here is a comment
                comment = " ".join(parts[i:]).lstrip("# ")
                break
            else:
                hostnames.append(part)

        if not hostnames:
            return None

        # Parse DNS metadata from comment
        dns_name = None
        dns_resolution_status = None
        last_resolved = None
        user_comment = None

        if comment:
            # Split comment by pipe (|) to separate DNS metadata from user comment
            comment_parts = [part.strip() for part in comment.split(' | ')]
            
            for part in comment_parts:
                if part.startswith('DNS:'):
                    # Parse DNS metadata: "DNS:example.com|Status:resolved|Last:2023-..."
                    dns_data = part.split('|')
                    for dns_part in dns_data:
                        if dns_part.startswith('DNS:'):
                            dns_name = dns_part[4:]  # Remove "DNS:" prefix
                        elif dns_part.startswith('Status:'):
                            dns_resolution_status = dns_part[7:]  # Remove "Status:" prefix
                        elif dns_part.startswith('Last:'):
                            try:
                                from datetime import datetime
                                last_resolved = datetime.fromisoformat(dns_part[5:])  # Remove "Last:" prefix
                            except (ValueError, TypeError):
                                pass  # Invalid datetime format, ignore
                else:
                    # This is a user comment part
                    if user_comment is None:
                        user_comment = part
                    else:
                        user_comment += f" | {part}"

        try:
            return cls(
                ip_address=ip_address,
                hostnames=hostnames,
                comment=user_comment,
                is_active=is_active,
                dns_name=dns_name,
                dns_resolution_status=dns_resolution_status,
                last_resolved=last_resolved,
            )
        except ValueError:
            # Skip invalid entries
            return None


@dataclass
class HostsFile:
    """
    Represents the complete hosts file structure.

    Attributes:
        entries: List of host entries
        header_comments: Comments at the beginning of the file
        footer_comments: Comments at the end of the file
    """

    entries: List[HostEntry] = field(default_factory=list)
    header_comments: List[str] = field(default_factory=list)
    footer_comments: List[str] = field(default_factory=list)

    def add_entry(self, entry: HostEntry) -> None:
        """Add a new entry to the hosts file."""
        entry.validate()
        self.entries.append(entry)

    def remove_entry(self, index: int) -> None:
        """Remove an entry by index."""
        if 0 <= index < len(self.entries):
            del self.entries[index]

    def toggle_entry(self, index: int) -> None:
        """Toggle the active state of an entry."""
        if 0 <= index < len(self.entries):
            self.entries[index].is_active = not self.entries[index].is_active

    def get_active_entries(self) -> List[HostEntry]:
        """Get all active entries."""
        return [entry for entry in self.entries if entry.is_active]

    def get_inactive_entries(self) -> List[HostEntry]:
        """Get all inactive entries."""
        return [entry for entry in self.entries if not entry.is_active]

    def get_dns_entries(self) -> List[HostEntry]:
        """Get all entries with DNS names configured."""
        return [entry for entry in self.entries if entry.has_dns_name()]

    def get_ip_entries(self) -> List[HostEntry]:
        """Get all entries with direct IP addresses (no DNS names)."""
        return [entry for entry in self.entries if not entry.has_dns_name()]

    def get_entries_needing_resolution(self) -> List[HostEntry]:
        """Get all entries that need DNS resolution."""
        return [entry for entry in self.entries if entry.needs_dns_resolution()]

    def get_stale_dns_entries(self, max_age_seconds: int = 300) -> List[HostEntry]:
        """Get all entries with stale DNS resolution."""
        return [entry for entry in self.entries if entry.has_dns_name() and entry.is_dns_resolution_stale(max_age_seconds)]

    def sort_by_ip(self, ascending: bool = True) -> None:
        """
        Sort entries by IP address, keeping default entries on top in fixed order.

        Args:
            ascending: Sort in ascending order if True, descending if False
        """
        # Separate default and non-default entries
        default_entries = [entry for entry in self.entries if entry.is_default_entry()]
        non_default_entries = [
            entry for entry in self.entries if not entry.is_default_entry()
        ]

        def ip_sort_key(entry):
            try:
                ip_str = entry.ip_address.lstrip("# ")
                ip_obj = ipaddress.ip_address(ip_str)
                # Create a tuple for sorting: (version, ip_int)
                return (ip_obj.version, int(ip_obj))
            except ValueError:
                # If IP parsing fails, use string comparison
                return (999, entry.ip_address)

        # Keep default entries in their natural fixed order (don't sort them)
        # Define the fixed order for default entries
        default_order = [
            {"ip": "127.0.0.1", "hostname": "localhost"},
            {"ip": "255.255.255.255", "hostname": "broadcasthost"},
            {"ip": "::1", "hostname": "localhost"},
        ]

        # Sort default entries according to their fixed order
        def default_sort_key(entry):
            for i, default in enumerate(default_order):
                if (
                    entry.ip_address == default["ip"]
                    and entry.hostnames
                    and entry.hostnames[0] == default["hostname"]
                ):
                    return i
            return 999  # fallback for any unexpected default entries

        default_entries.sort(key=default_sort_key)

        # Sort non-default entries according to the specified direction
        non_default_entries.sort(key=ip_sort_key, reverse=not ascending)

        # Combine: default entries always first, then sorted non-default entries
        self.entries = default_entries + non_default_entries

    def sort_by_hostname(self, ascending: bool = True) -> None:
        """
        Sort entries by first hostname, keeping default entries on top in fixed order.

        Args:
            ascending: Sort in ascending order if True, descending if False
        """
        # Separate default and non-default entries
        default_entries = [entry for entry in self.entries if entry.is_default_entry()]
        non_default_entries = [
            entry for entry in self.entries if not entry.is_default_entry()
        ]

        def hostname_sort_key(entry):
            hostname = (entry.hostnames[0] if entry.hostnames else "").lower()
            return hostname

        # Keep default entries in their natural fixed order (don't sort them)
        # Define the fixed order for default entries
        default_order = [
            {"ip": "127.0.0.1", "hostname": "localhost"},
            {"ip": "255.255.255.255", "hostname": "broadcasthost"},
            {"ip": "::1", "hostname": "localhost"},
        ]

        # Sort default entries according to their fixed order
        def default_sort_key(entry):
            for i, default in enumerate(default_order):
                if (
                    entry.ip_address == default["ip"]
                    and entry.hostnames
                    and entry.hostnames[0] == default["hostname"]
                ):
                    return i
            return 999  # fallback for any unexpected default entries

        default_entries.sort(key=default_sort_key)

        # Sort non-default entries according to the specified direction
        non_default_entries.sort(key=hostname_sort_key, reverse=not ascending)

        # Combine: default entries always first, then sorted non-default entries
        self.entries = default_entries + non_default_entries

    def find_entries_by_hostname(self, hostname: str) -> List[int]:
        """
        Find entry indices that contain the given hostname.

        Args:
            hostname: Hostname to search for

        Returns:
            List of indices where the hostname is found
        """
        indices = []
        for i, entry in enumerate(self.entries):
            if hostname.lower() in [h.lower() for h in entry.hostnames]:
                indices.append(i)
        return indices

    def find_entries_by_ip(self, ip_address: str) -> List[int]:
        """
        Find entry indices that have the given IP address.

        Args:
            ip_address: IP address to search for

        Returns:
            List of indices where the IP is found
        """
        indices = []
        for i, entry in enumerate(self.entries):
            if entry.ip_address == ip_address:
                indices.append(i)
        return indices
