package source

import (
	"sort"
	"strings"
	"time"

	"github.com/pranshuparmar/witr/pkg/model"
)

var suspiciousDirs = map[string]bool{"/": true, "/tmp": true, "/var/tmp": true}

var dangerousCapabilities = map[string]bool{
	"CAP_SYS_ADMIN":       true,
	"CAP_SYS_PTRACE":      true,
	"CAP_NET_RAW":         true,
	"CAP_DAC_OVERRIDE":    true,
	"CAP_DAC_READ_SEARCH": true,
	"CAP_FOWNER":          true,
	"CAP_SYS_MODULE":      true,
	"CAP_SYS_RAWIO":       true,
}

func isDangerousCapability(cap string) bool {
	return dangerousCapabilities[cap]
}

type envSuspiciousRule struct {
	pattern     string
	match       func(key, pattern string) bool
	warning     string
	includeKeys bool
}

var (
	envVarRules = []envSuspiciousRule{
		{
			pattern: "LD_PRELOAD",
			match:   func(key, pattern string) bool { return key == pattern },
			warning: "Process sets LD_PRELOAD (potential library injection)",
		},

		{
			pattern:     "DYLD_",
			match:       strings.HasPrefix,
			warning:     "Process sets DYLD_* variables (potential library injection)",
			includeKeys: true,
		},
	}
)

func Detect(ancestry []model.Process) model.Source {
	// Detection order prioritizes platform-specific init systems
	// over generic supervisor detection to avoid false positives
	if src := detectContainer(ancestry); src != nil {
		return *src
	}
	if src := detectSSH(ancestry); src != nil {
		return *src
	}
	if src := detectShell(ancestry); src != nil {
		return *src
	}
	if src := detectSystemd(ancestry); src != nil {
		return *src
	}
	if src := detectLaunchd(ancestry); src != nil {
		return *src
	}
	if src := detectBsdRc(ancestry); src != nil {
		return *src
	}
	if src := detectSupervisor(ancestry); src != nil {
		return *src
	}
	if src := detectCron(ancestry); src != nil {
		return *src
	}
	if src := detectWindowsService(ancestry); src != nil {
		return *src
	}
	if src := detectInit(ancestry); src != nil {
		return *src
	}

	return model.Source{
		Type: model.SourceUnknown,
	}
}

// env suspicious warnings returns warnings for known env based library injection patterns
func envSuspiciousWarnings(env []string) []string {
	matched := make([]bool, len(envVarRules))
	matchedKeys := make([]map[string]struct{}, len(envVarRules))

	// init per rule key capture only for rules that include keys
	for i, rule := range envVarRules {
		if rule.includeKeys {
			matchedKeys[i] = map[string]struct{}{}
		}
	}

	// scan env entries and record which rules match
	for _, entry := range env {
		key, value, ok := strings.Cut(entry, "=")
		if !ok || value == "" {
			continue
		}

		// check this key against each configured rule
		for i, rule := range envVarRules {
			if !rule.match(key, rule.pattern) {
				continue
			}
			matched[i] = true
			if rule.includeKeys {
				matchedKeys[i][key] = struct{}{}
			}
		}
	}

	var warnings []string

	// emit warnings in the same order as envVarRules
	for i, rule := range envVarRules {
		if !matched[i] {
			continue
		}
		if !rule.includeKeys {
			warnings = append(warnings, rule.warning)
			continue
		}

		keys := make([]string, 0, len(matchedKeys[i]))
		// collect all matched keys for this rule
		for key := range matchedKeys[i] {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		warnings = append(warnings, rule.warning+": "+strings.Join(keys, ", "))
	}

	return warnings
}

func Warnings(p []model.Process, srcType ...model.SourceType) []string {
	if len(p) == 0 {
		return nil
	}

	var w []string

	last := p[len(p)-1]

	// Restart count detection (count consecutive same-command entries)
	restartCount := 0
	lastCmd := ""
	for _, proc := range p {
		if proc.Command == lastCmd {
			restartCount++
		}
		lastCmd = proc.Command
	}
	if restartCount > 5 {
		w = append(w, "Process or ancestor restarted more than 5 times")
	}

	// Health warnings
	switch last.Health {
	case "zombie":
		w = append(w, "Process is a zombie (defunct)")
	case "stopped":
		w = append(w, "Process is stopped (T state)")
	case "high-cpu":
		w = append(w, "Process is using high CPU (>2h total)")
	case "high-mem":
		w = append(w, "Process is using high memory (>1GB RSS)")
	}

	if IsPublicBind(last.BindAddresses) {
		w = append(w, "Process is listening on a public interface")
	}

	if last.User == "root" {
		w = append(w, "Process is running as root")
	} else if len(last.Capabilities) > 0 {
		var dangerous []string
		for _, cap := range last.Capabilities {
			if isDangerousCapability(cap) {
				dangerous = append(dangerous, cap)
			}
		}
		if len(dangerous) > 0 {
			w = append(w, "Process has dangerous capabilities: "+strings.Join(dangerous, ", "))
		}
	}

	st := model.SourceUnknown
	if len(srcType) > 0 {
		st = srcType[0]
	} else {
		st = Detect(p).Type
	}
	if st == model.SourceUnknown {
		w = append(w, "No known supervisor or service manager detected")
	}

	// Warn if process is very old (>90 days)
	if time.Since(last.StartedAt).Hours() > 90*24 {
		w = append(w, "Process has been running for over 90 days")
	}

	if suspiciousDirs[last.WorkingDir] {
		w = append(w, "Process is running from a suspicious working directory: "+last.WorkingDir)
	}

	// Warn if container and no healthcheck (skip for snap/flatpak which don't use healthchecks)
	if last.Container != "" && !strings.HasPrefix(last.Container, "snap:") && !strings.HasPrefix(last.Container, "flatpak:") {
		w = append(w, "No healthcheck detected for container (best effort)")
	}

	// Warn if service name and process name are genuinely unrelated
	if last.Service != "" && last.Command != "" {
		svcCore := last.Service
		for _, suffix := range []string{".service", ".socket", ".timer", ".scope", ".slice", ".plist"} {
			svcCore = strings.TrimSuffix(svcCore, suffix)
		}
		svcCore = strings.ToLower(svcCore)
		cmdBase := strings.ToLower(last.Command)
		if !strings.Contains(svcCore, cmdBase) && !strings.Contains(cmdBase, svcCore) {
			w = append(w, "Service name and process name do not match")
		}
	}

	// Warn if binary is deleted
	if last.ExeDeleted {
		w = append(w, "Process is running from a deleted binary (potential library injection or pending update)")
	}

	// Include warnings based on suspicious env variables
	w = append(w, envSuspiciousWarnings(last.Env)...)

	return w
}

// EnrichSocketInfo provides human-readable explanations and workarounds for socket states
func EnrichSocketInfo(si *model.SocketInfo) {
	if si == nil {
		return
	}

	switch si.State {
	case "TIME_WAIT":
		si.Explanation = "The local OS is holding the port in a protocol-wait state to ensure all packets are received."
		si.Workaround = "Wait ~60s for the OS to release it, or enable SO_REUSEADDR in your code."
	case "CLOSE_WAIT":
		si.Explanation = "The remote end has closed the connection, but the local application hasn't responded."
		si.Workaround = "This usually indicates a resource leak in the application. Restart the process."
	case "FIN_WAIT_1", "FIN_WAIT_2":
		si.Explanation = "The connection is in the process of being closed."
	case "ESTABLISHED":
		si.Explanation = "The connection is active and data can be transferred."
	case "LISTEN":
		si.Explanation = "The process is actively waiting for incoming connections."
	}
}
