package main

import (
	"bufio"
	"encoding/json"
	"os"
	"regexp"
	"strings"
	"time"
)

// Config mirrors the shell-sourced awg_config file. The on-disk format is kept
// byte-compatible with the original bash profiler so the two tools can share it.
type Config struct {
	Network    string // SERVER_NETWORK
	Interface  string // SERVER_INTERFACE
	Port       string // SERVER_PUBLIC_PORT
	PublicIP   string // SERVER_PUBLIC_IP
	ServerPriv string // SERVER_PRIVATE_KEY
	ServerPub  string // SERVER_PUBLIC_KEY
	DNS        string // DNS_SERVER
	MTU        string // SERVER_MTU

	// AmneziaWG obfuscation parameters (shared by server and every client).
	Jc   string // AWG_JC
	Jmin string // AWG_JMIN
	Jmax string // AWG_JMAX
	S1   string // AWG_S1
	S2   string // AWG_S2
	H1   string // AWG_H1
	H2   string // AWG_H2
	H3   string // AWG_H3
	H4   string // AWG_H4
}

// mtuOr returns the configured MTU or the given fallback (bash: ${SERVER_MTU:-1420}).
func (c *Config) mtuOr(def string) string {
	if c.MTU == "" {
		return def
	}
	return c.MTU
}

var shellAssignRe = regexp.MustCompile(`^([A-Za-z_][A-Za-z0-9_]*)=(.*)$`)

// loadConfig parses the profiler config file, aborting if it is missing.
func loadConfig() *Config {
	f, err := os.Open(configFile)
	if err != nil {
		die("Config not found: run 'init-server' first")
	}
	defer f.Close()

	vals := map[string]string{}
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		m := shellAssignRe.FindStringSubmatch(line)
		if m == nil {
			continue
		}
		vals[m[1]] = unquoteShell(m[2])
	}

	return &Config{
		Network:    vals["SERVER_NETWORK"],
		Interface:  vals["SERVER_INTERFACE"],
		Port:       vals["SERVER_PUBLIC_PORT"],
		PublicIP:   vals["SERVER_PUBLIC_IP"],
		ServerPriv: vals["SERVER_PRIVATE_KEY"],
		ServerPub:  vals["SERVER_PUBLIC_KEY"],
		DNS:        vals["DNS_SERVER"],
		MTU:        vals["SERVER_MTU"],
		Jc:         vals["AWG_JC"],
		Jmin:       vals["AWG_JMIN"],
		Jmax:       vals["AWG_JMAX"],
		S1:         vals["AWG_S1"],
		S2:         vals["AWG_S2"],
		H1:         vals["AWG_H1"],
		H2:         vals["AWG_H2"],
		H3:         vals["AWG_H3"],
		H4:         vals["AWG_H4"],
	}
}

// unquoteShell strips a single layer of surrounding single/double quotes.
func unquoteShell(s string) string {
	s = strings.TrimSpace(s)
	if len(s) >= 2 {
		if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
			return s[1 : len(s)-1]
		}
	}
	return s
}

// writeConfig serialises the config in the original shell-sourceable format.
func writeConfig(c *Config) {
	now := time.Now().UTC().Format("2006-01-02 15:04:05") + " UTC"
	content := `# AmneziaWG Profiler Configuration
# Generated: ` + now + `

# Server network (CIDR) — server takes .1, clients get .2+
SERVER_NETWORK="` + c.Network + `"

# AmneziaWG interface name
SERVER_INTERFACE="` + c.Interface + `"

# Server listen port
SERVER_PUBLIC_PORT="` + c.Port + `"

# Server public IP or hostname (used in client configs)
SERVER_PUBLIC_IP="` + c.PublicIP + `"

# Server keys (generated by init-server)
SERVER_PRIVATE_KEY="` + c.ServerPriv + `"
SERVER_PUBLIC_KEY="` + c.ServerPub + `"

# Client DNS
DNS_SERVER="` + c.DNS + `"

# MTU
SERVER_MTU="` + c.MTU + `"

# ─── AmneziaWG obfuscation parameters ───────────────────────────────
# These MUST be identical on server and every client to interoperate.
# Jc   : junk packet count               (1-128, recommended 4-12)
# Jmin : min junk packet size            (< Jmax, < 1280)
# Jmax : max junk packet size            (> Jmin, <= 1280)
# S1   : init packet junk size           (<= 1132)
# S2   : response packet junk size       (<= 1188, and S1 + 56 != S2)
# H1-H4: magic header values             (5..2147483647, all distinct)
AWG_JC="` + c.Jc + `"
AWG_JMIN="` + c.Jmin + `"
AWG_JMAX="` + c.Jmax + `"
AWG_S1="` + c.S1 + `"
AWG_S2="` + c.S2 + `"
AWG_H1="` + c.H1 + `"
AWG_H2="` + c.H2 + `"
AWG_H3="` + c.H3 + `"
AWG_H4="` + c.H4 + `"
`
	if err := os.WriteFile(configFile, []byte(content), 0600); err != nil {
		die("Failed to write config: %v", err)
	}
	info("Config written: %s", configFile)
}

// ─── deps-installed state (new: separates install-deps from init-server) ─────────

// State records whether the dependency-installation step has been completed.
// It lives in its own file so `install-deps` can run before any server config
// exists, and `init-server` can verify the flag without re-installing anything.
type State struct {
	DepsInstalled bool   `json:"deps_installed"`
	InstalledAt   string `json:"installed_at,omitempty"`
	OSID          string `json:"os_id,omitempty"`
	OSVersion     string `json:"os_version,omitempty"`
}

// loadState reads the state file, returning a zero value if it is absent.
func loadState() *State {
	data, err := os.ReadFile(stateFile)
	if err != nil {
		return &State{}
	}
	var s State
	if err := json.Unmarshal(data, &s); err != nil {
		warn("State file %s is invalid — treating as empty", stateFile)
		return &State{}
	}
	return &s
}

// saveState persists the state file with restrictive permissions.
func saveState(s *State) {
	data, err := json.MarshalIndent(s, "", "  ")
	if err != nil {
		die("Failed to encode state: %v", err)
	}
	if err := os.WriteFile(stateFile, append(data, '\n'), 0600); err != nil {
		die("Failed to write state file: %v", err)
	}
}
