package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"ekdns/hostfile"
	"ekdns/lockfile"
	"ekdns/platform"
	"ekdns/resolver"
)

// version and copyrightYear are set at build time via -ldflags.
var version = "dev"
var copyrightYear = "2024"

func main() {
	// Banner (always printed to stdout).
	fmt.Printf("ekDNSHelper %s\n", version)
	fmt.Printf("Copyright (c) %s Emberkom LLC\n", copyrightYear)
	fmt.Println("")

	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}

	switch strings.ToLower(os.Args[1]) {
	case "a", "add":
		os.Exit(runAdd(os.Args[2:]))
	case "d", "del", "delete":
		os.Exit(runDelete(os.Args[2:]))
	default:
		printUsage()
		os.Exit(1)
	}
}

func runAdd(args []string) int {
	fs := flag.NewFlagSet("add", flag.ContinueOnError)
	hostFlag := fs.String("host", "", "Comma separated list of hostnames to add")
	serverFlag := fs.String("server", "", "Resolution mode: omit for smart default, local, gateway, or IP/IP:port")
	timeoutFlag := fs.Int("timeout", 3, "Per-query DNS timeout in seconds")
	verboseFlag := fs.Bool("verbose", false, "Emit per-stage resolution trace to stderr")
	if err := fs.Parse(args); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	// Validation (FR-006).
	if *hostFlag == "" {
		fmt.Fprintf(os.Stderr, "Error: -host flag is required for the add command\n")
		return 1
	}
	if *timeoutFlag <= 0 {
		fmt.Fprintf(os.Stderr, "Error: -timeout must be a positive integer\n")
		return 1
	}

	mode, err := resolver.ParseServerFlag(*serverFlag)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	config := resolver.QueryConfig{
		Timeout: time.Duration(*timeoutFlag) * time.Second,
		Verbose: *verboseFlag,
	}
	discoverer := platform.NewNetworkDiscoverer()

	hostnames := strings.Split(*hostFlag, ",")

	// Step 1: DNS resolution before lock acquisition (FR-016).
	var entries []hostfile.DNSEntry
	var warnings []string
	for _, h := range hostnames {
		h = strings.TrimSpace(h)
		ips, err := resolver.Resolve(h, mode, config, discoverer)
		if err != nil {
			warnings = append(warnings, fmt.Sprintf("Warning: failed to resolve %s: %v", h, err))
			continue
		}
		for _, ip := range ips {
			entries = append(entries, hostfile.DNSEntry{IP: ip, Hostname: h})
		}
	}

	if len(entries) == 0 && len(warnings) > 0 {
		for _, w := range warnings {
			fmt.Fprintln(os.Stderr, w)
		}
		return 1
	}

	// Step 2: Get hosts file path.
	hostsPath, err := platform.GetHostsFilePath()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	// Step 3: Get executable directory for lock and backup storage.
	exePath, err := os.Executable()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: determining executable path: %v\n", err)
		return 1
	}
	exeDir := filepath.Dir(exePath)

	// Step 4: Acquire lock (FR-016).
	lock, err := lockfile.Acquire(exeDir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}
	defer lock.Release()

	// Step 5: Read, modify, write.
	mgr := hostfile.NewManager(hostfile.OSFileSystem{})
	hf, err := mgr.Read(hostsPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	hf.ManagedContent = hostfile.AddEntries(hf.ManagedContent, entries)

	if err := mgr.Write(hf, exeDir); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	// Step 6: Output summary (FR-013).
	entryCounts := make(map[string]int)
	for _, e := range entries {
		entryCounts[e.Hostname]++
	}
	for _, h := range hostnames {
		h = strings.TrimSpace(h)
		if count, ok := entryCounts[h]; ok {
			fmt.Printf("Added %d entries for %s\n", count, h)
		}
	}
	for _, w := range warnings {
		fmt.Fprintln(os.Stderr, w)
	}

	if len(warnings) > 0 {
		return 1 // Partial failure (FR-015).
	}
	return 0
}

func runDelete(args []string) int {
	// Check for "all" keyword first — positional argument, not a flag.
	if len(args) > 0 && (strings.ToLower(args[0]) == "all" || strings.ToLower(args[0]) == "a") {
		return runDeleteAll()
	}

	fs := flag.NewFlagSet("delete", flag.ContinueOnError)
	hostFlag := fs.String("host", "", "Comma separated list of hostnames to delete")
	if err := fs.Parse(args); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	if *hostFlag == "" {
		fmt.Fprintf(os.Stderr, "Error: -host flag is required for the delete command\n")
		return 1
	}

	hostnames := strings.Split(*hostFlag, ",")
	for i, h := range hostnames {
		hostnames[i] = strings.TrimSpace(h)
	}

	// Step 1: Get hosts file path.
	hostsPath, err := platform.GetHostsFilePath()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	// Step 2: Get executable directory.
	exePath, err := os.Executable()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: determining executable path: %v\n", err)
		return 1
	}
	exeDir := filepath.Dir(exePath)

	// Step 3: Acquire lock.
	lock, err := lockfile.Acquire(exeDir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}
	defer lock.Release()

	// Step 4: Read and parse hosts file.
	mgr := hostfile.NewManager(hostfile.OSFileSystem{})
	hf, err := mgr.Read(hostsPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	// Step 5: Remove entries by hostname, counting originals for summary.
	// Count original managed lines per hostname before removal.
	originalCounts := make(map[string]int)
	for _, h := range hostnames {
		for _, line := range hf.ManagedContent {
			if strings.Contains(line, h) {
				originalCounts[h]++
			}
		}
	}

	hf.ManagedContent = hostfile.RemoveByHostname(hf.ManagedContent, hostnames)

	// Step 6: Write updated file.
	if err := mgr.Write(hf, exeDir); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	// Step 7: Print per-hostname summary.
	for _, h := range hostnames {
		if count, ok := originalCounts[h]; ok && count > 0 {
			fmt.Printf("Removed %d entries for %s\n", count, h)
		} else {
			fmt.Printf("No managed entries found for %s\n", h)
		}
	}
	return 0
}

func runDeleteAll() int {
	// Step 1: Get hosts file path.
	hostsPath, err := platform.GetHostsFilePath()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	// Step 2: Get executable directory.
	exePath, err := os.Executable()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: determining executable path: %v\n", err)
		return 1
	}
	exeDir := filepath.Dir(exePath)

	// Step 3: Acquire lock.
	lock, err := lockfile.Acquire(exeDir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}
	defer lock.Release()

	// Step 4: Read and parse hosts file.
	mgr := hostfile.NewManager(hostfile.OSFileSystem{})
	hf, err := mgr.Read(hostsPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	// Step 5: Remove all managed entries.
	entryCount := len(hf.ManagedContent)
	hf.ManagedContent = hostfile.RemoveAll()

	// Step 6: Write updated file.
	if err := mgr.Write(hf, exeDir); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return 1
	}

	// Step 7: Print summary.
	if entryCount > 0 {
		fmt.Printf("Removed all managed entries (%d entries removed)\n", entryCount)
	} else {
		fmt.Println("No managed entries found")
	}
	return 0
}

func printUsage() {
	fmt.Println("Updates the local hosts file with DNS entries resolved by this tool.")
	fmt.Println("")
	fmt.Println("Usage:")
	fmt.Println("  ekdns add -host <hostnames> [-server <mode>] [-timeout <seconds>] [-verbose]")
	fmt.Println("  ekdns delete -host <hostnames>")
	fmt.Println("  ekdns delete all")
	fmt.Println("")
	fmt.Println("Flags (add):")
	fmt.Println("  -host      Comma-separated list of hostnames to resolve and add (required)")
	fmt.Println("  -server    Resolution mode (optional):")
	fmt.Println("               (omitted)   Smart default: NS fan-out → authoritative query → parallel A fallback")
	fmt.Println("               local       Query locally configured DNS resolvers only (no public fallback)")
	fmt.Println("               gateway     Query default gateway as DNS server (no fallback)")
	fmt.Println("               <ip>        Query this IP on port 53")
	fmt.Println("               <ip>:<port> Query this IP on the given port")
	fmt.Println("  -timeout   Per-query DNS timeout in seconds (default: 3)")
	fmt.Println("  -verbose   Emit per-stage resolution trace to stderr")
	fmt.Println("")
	fmt.Println("Examples:")
	fmt.Println("  ekdns add -host www.example.com")
	fmt.Println("    Resolve using smart default (NS fan-out + authoritative query).")
	fmt.Println("  ekdns add -host internal.client.local -server local")
	fmt.Println("    Resolve using only locally configured DNS resolvers.")
	fmt.Println("  ekdns add -host www.example.com -server gateway")
	fmt.Println("    Resolve via the default gateway IP.")
	fmt.Println("  ekdns add -host www.example.com -server 8.8.8.8")
	fmt.Println("    Resolve via a specific DNS server.")
	fmt.Println("  ekdns add -host www.example.com -server 8.8.8.8:5353 -timeout 5 -verbose")
	fmt.Println("    Resolve via 8.8.8.8:5353 with 5s timeout and verbose trace.")
	fmt.Println("  ekdns delete -host www.example.com")
	fmt.Println("    Remove all managed entries for www.example.com from the hosts file.")
	fmt.Println("  ekdns delete all")
	fmt.Println("    Remove all managed entries from the hosts file.")
	fmt.Println("")
	fmt.Println("Notes:")
	fmt.Println("  Adding a hostname first removes all existing managed entries for that hostname.")
	fmt.Println("  Only entries added by this tool are ever removed.")
}
