package cmd

import (
	"fmt"
	"github.com/karol-broda/snitch/internal/collector"
	"github.com/karol-broda/snitch/internal/color"
	"github.com/karol-broda/snitch/internal/config"
	"github.com/karol-broda/snitch/internal/resolver"
	"strconv"
	"strings"

	"github.com/spf13/cobra"
)

// Runtime holds the shared state for all commands.
// it handles common filter logic, fetching, filtering, and resolution.
type Runtime struct {
	// filter options built from flags and args
	Filters collector.FilterOptions

	// filtered connections ready for rendering
	Connections []collector.Connection

	// common settings
	ColorMode    string
	ResolveAddrs bool
	ResolvePorts bool
	NoCache      bool
}

// shared filter flags - used by all commands
var (
	filterTCP    bool
	filterUDP    bool
	filterListen bool
	filterEstab  bool
	filterIPv4   bool
	filterIPv6   bool
)

// shared resolution flags - used by all commands
var (
	resolveAddrs bool
	resolvePorts bool
	noCache      bool
)

// BuildFilters constructs FilterOptions from command args and shortcut flags.
func BuildFilters(args []string) (collector.FilterOptions, error) {
	filters, err := ParseFilterArgs(args)
	if err != nil {
		return filters, err
	}

	// apply ipv4/ipv6 flags
	filters.IPv4 = filterIPv4
	filters.IPv6 = filterIPv6

	// apply protocol shortcut flags
	if filterTCP && !filterUDP {
		filters.Proto = "tcp"
	} else if filterUDP && !filterTCP {
		filters.Proto = "udp"
	}

	// apply state shortcut flags
	if filterListen && !filterEstab {
		filters.State = "LISTEN"
	} else if filterEstab && !filterListen {
		filters.State = "ESTABLISHED"
	}

	return filters, nil
}

// FetchConnections gets connections from the collector and applies filters.
func FetchConnections(filters collector.FilterOptions) ([]collector.Connection, error) {
	connections, err := collector.GetConnections()
	if err != nil {
		return nil, err
	}

	return collector.FilterConnections(connections, filters), nil
}

// NewRuntime creates a runtime with fetched and filtered connections.
func NewRuntime(args []string, colorMode string) (*Runtime, error) {
	color.Init(colorMode)

	cfg := config.Get()
	
	// configure resolver with cache setting (flag overrides config)
	effectiveNoCache := noCache || !cfg.Defaults.DNSCache
	resolver.SetNoCache(effectiveNoCache)

	filters, err := BuildFilters(args)
	if err != nil {
		return nil, fmt.Errorf("failed to parse filters: %w", err)
	}

	connections, err := FetchConnections(filters)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch connections: %w", err)
	}

	rt := &Runtime{
		Filters:      filters,
		Connections:  connections,
		ColorMode:    colorMode,
		ResolveAddrs: resolveAddrs,
		ResolvePorts: resolvePorts,
		NoCache:      effectiveNoCache,
	}

	// pre-warm dns cache by resolving all addresses in parallel
	if resolveAddrs {
		rt.PreWarmDNS()
	}

	return rt, nil
}

// PreWarmDNS resolves all connection addresses in parallel to warm the cache.
func (r *Runtime) PreWarmDNS() {
	addrs := make([]string, 0, len(r.Connections)*2)
	for _, c := range r.Connections {
		addrs = append(addrs, c.Laddr, c.Raddr)
	}
	resolver.ResolveAddrsParallel(addrs)
}

// SortConnections sorts the runtime's connections in place.
func (r *Runtime) SortConnections(opts collector.SortOptions) {
	collector.SortConnections(r.Connections, opts)
}

// ParseFilterArgs parses key=value filter arguments.
// exported for testing.
func ParseFilterArgs(args []string) (collector.FilterOptions, error) {
	filters := collector.FilterOptions{}
	for _, arg := range args {
		parts := strings.SplitN(arg, "=", 2)
		if len(parts) != 2 {
			return filters, fmt.Errorf("invalid filter format: %s (expected key=value)", arg)
		}
		key, value := parts[0], parts[1]
		if err := applyFilter(&filters, key, value); err != nil {
			return filters, err
		}
	}
	return filters, nil
}

// applyFilter applies a single key=value filter to FilterOptions.
func applyFilter(filters *collector.FilterOptions, key, value string) error {
	switch strings.ToLower(key) {
	case "proto":
		filters.Proto = value
	case "state":
		filters.State = value
	case "pid":
		pid, err := strconv.Atoi(value)
		if err != nil {
			return fmt.Errorf("invalid pid value: %s", value)
		}
		filters.Pid = pid
	case "proc":
		filters.Proc = value
	case "lport":
		port, err := strconv.Atoi(value)
		if err != nil {
			return fmt.Errorf("invalid lport value: %s", value)
		}
		filters.Lport = port
	case "rport":
		port, err := strconv.Atoi(value)
		if err != nil {
			return fmt.Errorf("invalid rport value: %s", value)
		}
		filters.Rport = port
	case "user":
		uid, err := strconv.Atoi(value)
		if err == nil {
			filters.UID = uid
		} else {
			filters.User = value
		}
	case "laddr":
		filters.Laddr = value
	case "raddr":
		filters.Raddr = value
	case "contains":
		filters.Contains = value
	case "if", "interface":
		filters.Interface = value
	case "mark":
		filters.Mark = value
	case "namespace":
		filters.Namespace = value
	case "inode":
		inode, err := strconv.ParseInt(value, 10, 64)
		if err != nil {
			return fmt.Errorf("invalid inode value: %s", value)
		}
		filters.Inode = inode
	case "since":
		since, sinceRel, err := collector.ParseTimeFilter(value)
		if err != nil {
			return fmt.Errorf("invalid since value: %s", value)
		}
		filters.Since = since
		filters.SinceRel = sinceRel
	default:
		return fmt.Errorf("unknown filter key: %s", key)
	}
	return nil
}

// FilterFlagsHelp returns the help text for common filter flags.
const FilterFlagsHelp = `
Filters are specified in key=value format. For example:
  snitch ls proto=tcp state=established

Available filters:
  proto, state, pid, proc, lport, rport, user, laddr, raddr, contains, if, mark, namespace, inode, since`

// addFilterFlags adds the common filter flags to a command.
func addFilterFlags(cmd *cobra.Command) {
	cmd.Flags().BoolVarP(&filterTCP, "tcp", "t", false, "Show only TCP connections")
	cmd.Flags().BoolVarP(&filterUDP, "udp", "u", false, "Show only UDP connections")
	cmd.Flags().BoolVarP(&filterListen, "listen", "l", false, "Show only listening sockets")
	cmd.Flags().BoolVarP(&filterEstab, "established", "e", false, "Show only established connections")
	cmd.Flags().BoolVarP(&filterIPv4, "ipv4", "4", false, "Only show IPv4 connections")
	cmd.Flags().BoolVarP(&filterIPv6, "ipv6", "6", false, "Only show IPv6 connections")
}

// addResolutionFlags adds the common resolution flags to a command.
func addResolutionFlags(cmd *cobra.Command) {
	cfg := config.Get()
	cmd.Flags().BoolVar(&resolveAddrs, "resolve-addrs", !cfg.Defaults.Numeric, "Resolve IP addresses to hostnames")
	cmd.Flags().BoolVar(&resolvePorts, "resolve-ports", false, "Resolve port numbers to service names")
	cmd.Flags().BoolVar(&noCache, "no-cache", !cfg.Defaults.DNSCache, "Disable DNS caching (force fresh lookups)")
}

