# CoreDNS Zone File Management
# Create, update, and manage DNS zone files

use ../utils/log.nu *
use corefile.nu generate-zone-file

# Create zone file with SOA and NS records
export def create-zone-file [
    zone_name: string      # Zone name (e.g., "provisioning.local")
    zones_path: string     # Path to zones directory
    --config: record = {}  # Optional configuration
] -> bool {
    log info $"Creating zone file for ($zone_name)"

    let expanded_path = $zones_path | path expand
    let zone_file = $"($expanded_path)/($zone_name).zone"

    # Create zones directory if not exists
    if not ($expanded_path | path exists) {
        mkdir $expanded_path
    }

    # Generate initial zone file with SOA and NS records
    let zone_content = generate-zone-file $zone_name [] $config

    let result = (do {
        $zone_content | save -f $zone_file
        true
    } | complete)

    if $result.exit_code == 0 {
        log info $"Zone file created: [$zone_file]"
        true
    } else {
        log error $"Failed to create zone file: [$zone_file]"
        false
    }
}

# Add A record to zone
export def add-a-record [
    zone_name: string     # Zone name
    hostname: string      # Hostname (without domain)
    ip_address: string    # IPv4 address
    --zones-path: string = "~/.provisioning/coredns/zones"
    --ttl: int = 300
    --comment: string = ""
] -> bool {
    log info $"Adding A record: ($hostname) -> ($ip_address)"

    let record = {
        name: $hostname
        type: "A"
        value: $ip_address
        ttl: $ttl
        comment: $comment
    }

    add-record $zone_name $record --zones-path $zones_path
}

# Add AAAA record to zone
export def add-aaaa-record [
    zone_name: string     # Zone name
    hostname: string      # Hostname
    ipv6_address: string  # IPv6 address
    --zones-path: string = "~/.provisioning/coredns/zones"
    --ttl: int = 300
    --comment: string = ""
] -> bool {
    log info $"Adding AAAA record: ($hostname) -> ($ipv6_address)"

    let record = {
        name: $hostname
        type: "AAAA"
        value: $ipv6_address
        ttl: $ttl
        comment: $comment
    }

    add-record $zone_name $record --zones-path $zones_path
}

# Add CNAME record to zone
export def add-cname-record [
    zone_name: string     # Zone name
    alias: string         # Alias name
    target: string        # Target hostname
    --zones-path: string = "~/.provisioning/coredns/zones"
    --ttl: int = 300
    --comment: string = ""
] -> bool {
    log info $"Adding CNAME record: ($alias) -> ($target)"

    let record = {
        name: $alias
        type: "CNAME"
        value: $target
        ttl: $ttl
        comment: $comment
    }

    add-record $zone_name $record --zones-path $zones_path
}

# Add MX record to zone
export def add-mx-record [
    zone_name: string     # Zone name
    hostname: string      # Hostname (@ for zone apex)
    mail_server: string   # Mail server hostname
    priority: int         # Priority (lower is higher priority)
    --zones-path: string = "~/.provisioning/coredns/zones"
    --ttl: int = 300
    --comment: string = ""
] -> bool {
    log info $"Adding MX record: ($hostname) -> ($mail_server) (priority: ($priority))"

    let record = {
        name: $hostname
        type: "MX"
        value: $mail_server
        priority: $priority
        ttl: $ttl
        comment: $comment
    }

    add-record $zone_name $record --zones-path $zones_path
}

# Add TXT record to zone
export def add-txt-record [
    zone_name: string     # Zone name
    hostname: string      # Hostname
    text: string          # Text value
    --zones-path: string = "~/.provisioning/coredns/zones"
    --ttl: int = 300
    --comment: string = ""
] -> bool {
    log info $"Adding TXT record: ($hostname) -> ($text)"

    let record = {
        name: $hostname
        type: "TXT"
        value: $"\"($text)\""
        ttl: $ttl
        comment: $comment
    }

    add-record $zone_name $record --zones-path $zones_path
}

# Generic add record function
def add-record [
    zone_name: string
    record: record
    --zones-path: string
] -> bool {
    let expanded_path = $zones_path | path expand
    let zone_file = $"($expanded_path)/($zone_name).zone"

    if not ($zone_file | path exists) {
        log error $"Zone file not found: ($zone_file)"
        return false
    }

    # Read existing zone file
    let content = open $zone_file

    # Check if record already exists
    let record_name = $record.name
    let record_pattern = $"^($record_name)\\s+"

    if ($content | str contains -r $record_pattern) {
        log warn $"Record ($record_name) already exists, updating..."
        remove-record $zone_name $record_name --zones-path $zones_path
    }

    # Generate record line
    let record_line = generate-record-line $record

    # Find insertion point (after NS records, before other records)
    let lines = $content | lines
    mut updated_lines = []
    mut inserted = false

    for line in $lines {
        $updated_lines = ($updated_lines | append $line)

        # Insert after NS records section
        if (not $inserted) and ($line | str contains "IN      NS") {
            $updated_lines = ($updated_lines | append "")
            $updated_lines = ($updated_lines | append $record_line)
            $inserted = true
        }
    }

    # If no NS record found, append at end
    if not $inserted {
        $updated_lines = ($updated_lines | append "")
        $updated_lines = ($updated_lines | append $record_line)
    }

    # Increment serial number
    $updated_lines = (increment-zone-serial $updated_lines)

    # Write updated zone file
    let result = (do {
        $updated_lines | str join "\n" | save -f $zone_file
        true
    } | complete)

    if $result.exit_code == 0 {
        log info $"Record added to [$zone_file]"
        true
    } else {
        log error $"Failed to update zone file: [$zone_file]"
        false
    }
}

# Remove record from zone
export def remove-record [
    zone_name: string     # Zone name
    hostname: string      # Hostname to remove
    --zones-path: string = "~/.provisioning/coredns/zones"
] -> bool {
    log info $"Removing record: ($hostname)"

    let expanded_path = $zones_path | path expand
    let zone_file = $"($expanded_path)/($zone_name).zone"

    if not ($zone_file | path exists) {
        log error $"Zone file not found: ($zone_file)"
        return false
    }

    let content = open $zone_file
    let lines = $content | lines

    # Filter out lines matching hostname
    let record_pattern = $"^($hostname)\\s+"
    let filtered = $lines | where {|line| not ($line | str contains -r $record_pattern)}

    # Increment serial number
    let updated = increment-zone-serial $filtered

    let result = (do {
        $updated | str join "\n" | save -f $zone_file
        true
    } | complete)

    if $result.exit_code == 0 {
        log info $"Record removed from [$zone_file]"
        true
    } else {
        log error $"Failed to update zone file: [$zone_file]"
        false
    }
}

# List all records in zone
export def list-zone-records [
    zone_name: string      # Zone name
    --zones-path: string = "~/.provisioning/coredns/zones"
    --format: string = "table"  # Output format: table, json, yaml
] -> any {
    let expanded_path = $zones_path | path expand
    let zone_file = $"($expanded_path)/($zone_name).zone"

    if not ($zone_file | path exists) {
        log error $"Zone file not found: ($zone_file)"
        return []
    }

    let content = open $zone_file
    let lines = $content | lines

    # Parse DNS records
    mut records = []

    for line in $lines {
        # Skip comments, empty lines, and directives
        if ($line | str starts-with ";") or ($line | str starts-with "$") or ($line | str trim | is-empty) {
            continue
        }

        # Skip SOA record (multi-line)
        if ($line | str contains "SOA") {
            continue
        }

        # Parse record line
        let parsed = parse-record-line $line

        if ($parsed | is-not-empty) {
            $records = ($records | append $parsed)
        }
    }

    # Format output
    match $format {
        "json" => { $records | to json }
        "yaml" => { $records | to yaml }
        _ => { $records }
    }
}

# Parse DNS record line
def parse-record-line [
    line: string
] -> record {
    # Split line into parts
    let parts = $line | str trim | split row -r '\s+' | where {|x| $x != ""}

    if ($parts | length) < 4 {
        return {}
    }

    let name = $parts | get 0
    let class_or_ttl = $parts | get 1
    let type_or_class = $parts | get 2
    let type_or_value = $parts | get 3

    # Determine if TTL is present
    mut ttl = null
    mut record_type = ""
    mut value = ""

    if $class_or_ttl == "IN" {
        # No TTL: name IN type value
        $record_type = $type_or_class
        $value = $type_or_value
    } else {
        # With TTL: name ttl IN type value
        $ttl = $class_or_ttl | into int
        $record_type = $type_or_value
        if ($parts | length) >= 5 {
            $value = $parts | get 4
        }
    }

    {
        name: $name
        type: $record_type
        value: $value
        ttl: $ttl
    }
}

# Generate DNS record line (duplicated from corefile.nu for independence)
def generate-record-line [
    record: record
] -> string {
    let name = $record.name
    let type = $record.type
    let value = $record.value
    let ttl = $record.ttl? | default ""
    let priority = $record.priority? | default ""
    let comment = $record.comment? | default ""

    mut parts = []

    # Name (padded to 16 chars)
    $parts = ($parts | append ($name | fill -a left -w 16))

    # TTL (optional)
    if ($ttl != "") {
        $parts = ($parts | append ($ttl | into string | fill -a left -w 8))
    } else {
        $parts = ($parts | append ("" | fill -a left -w 8))
    }

    # Class (always IN)
    $parts = ($parts | append "IN")

    # Type
    $parts = ($parts | append ($type | fill -a left -w 8))

    # Priority (for MX and SRV)
    if ($priority != "") {
        $parts = ($parts | append ($priority | into string))
    }

    # Value
    $parts = ($parts | append $value)

    let record_line = $parts | str join " "

    # Add comment if present
    if ($comment != "") {
        $"($record_line)  ; ($comment)"
    } else {
        $record_line
    }
}

# Increment zone serial number
def increment-zone-serial [
    lines: list<string>
] -> list<string> {
    mut updated = []
    mut serial_incremented = false

    for line in $lines {
        if (not $serial_incremented) and ($line | str contains "; Serial") {
            # Extract current serial
            let serial_match = $line | parse -r '(\d+)\s*; Serial'

            if ($serial_match | is-not-empty) {
                let current_serial = $serial_match | get 0.capture0 | into int
                let new_serial = $current_serial + 1
                let updated_line = $line | str replace $"($current_serial)" $"($new_serial)"
                $updated = ($updated | append $updated_line)
                $serial_incremented = true
                continue
            }
        }

        $updated = ($updated | append $line)
    }

    $updated
}

# Reload zone file (signal CoreDNS to reload)
export def reload-zone [
    zone_name: string     # Zone name
] -> bool {
    log info $"Reloading zone: ($zone_name)"

    # Send SIGUSR1 to CoreDNS to reload
    let pid = get-coredns-pid

    if $pid == null {
        log warn "CoreDNS not running, no reload needed"
        return false
    }

    let result = (do {
        kill -s USR1 $pid
        true
    } | complete)

    if $result.exit_code == 0 {
        log info "CoreDNS reload signal sent"
        true
    } else {
        log error "Failed to reload CoreDNS"
        false
    }
}

# Get CoreDNS PID
def get-coredns-pid [] -> int {
    let pid_file = "~/.provisioning/coredns/coredns.pid" | path expand

    if ($pid_file | path exists) {
        open $pid_file | into int
    } else {
        # Try to find via ps
        let ps_result = ps | where name =~ "coredns"

        if ($ps_result | is-not-empty) {
            $ps_result | get 0.pid
        } else {
            null
        }
    }
}

# Validate zone file syntax
export def validate-zone-file [
    zone_name: string      # Zone name
    --zones-path: string = "~/.provisioning/coredns/zones"
] -> record {
    log debug $"Validating zone file for ($zone_name)"

    let expanded_path = $zones_path | path expand
    let zone_file = $"($expanded_path)/($zone_name).zone"

    if not ($zone_file | path exists) {
        return {
            valid: false
            errors: ["Zone file not found"]
            warnings: []
        }
    }

    mut errors = []
    mut warnings = []

    let content = open $zone_file
    let lines = $content | lines

    # Check for required records
    let has_soa = $lines | any {|line| $line | str contains "SOA"}
    let has_ns = $lines | any {|line| $line | str contains "IN      NS"}

    if not $has_soa {
        $errors = ($errors | append "Missing SOA record")
    }

    if not $has_ns {
        $errors = ($errors | append "Missing NS record")
    }

    # Check for $ORIGIN directive
    let has_origin = $lines | any {|line| $line | str starts-with "$ORIGIN"}

    if not $has_origin {
        $warnings = ($warnings | append "Missing $ORIGIN directive")
    }

    {
        valid: ($errors | is-empty)
        errors: $errors
        warnings: $warnings
    }
}

# Backup zone file
export def backup-zone-file [
    zone_name: string      # Zone name
    --zones-path: string = "~/.provisioning/coredns/zones"
] -> bool {
    let expanded_path = $zones_path | path expand
    let zone_file = $"($expanded_path)/($zone_name).zone"

    if not ($zone_file | path exists) {
        log error $"Zone file not found: ($zone_file)"
        return false
    }

    let timestamp = date now | format date "%Y%m%d-%H%M%S"
    let backup_file = $"($zone_file).($timestamp).bak"

    let result = (do {
        cp $zone_file $backup_file
        true
    } | complete)

    if $result.exit_code == 0 {
        log info $"Zone file backed up to [$backup_file]"
        true
    } else {
        log error "Failed to backup zone file"
        false
    }
}
