package infra

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

// WGPeerAdd añade un peer WireGuard al wg0.conf del hub y aplica la config
// en caliente con `wg syncconf` sin reiniciar la interface.
//
// Idempotente:
//   - Si PublicKey ya está presente con la misma config → "already-present".
//   - Si DeviceID existe con otra PublicKey → reemplaza el bloque → "reconfigured".
//   - Si AllowedIPs está vacío, asigna la primera IP libre de subnetCIDR (excluyendo .1).
//
// Escribe atómicamente (tmpfile + rename) y hace chmod 600 sobre configPath.
// Si syncconf falla, restaura el backup y devuelve error.
//
// Para tests CI sin WireGuard real, establecer WG_SKIP_SYNCCONF=1.
func WGPeerAdd(spec WGPeerSpec, configPath string, subnetCIDR string) (WGPeerResult, error) {
	// --- leer config actual ---
	existing, err := os.ReadFile(configPath)
	if err != nil && !os.IsNotExist(err) {
		return WGPeerResult{}, fmt.Errorf("wg_peer_add: read config: %w", err)
	}
	content := string(existing)

	// --- parsear peers existentes ---
	peers, err := wgParsePeers(content)
	if err != nil {
		return WGPeerResult{}, fmt.Errorf("wg_peer_add: parse peers: %w", err)
	}

	// --- idempotencia: buscar peer existente ANTES de asignar IP ---
	status := "added"
	existingBlock, foundByKey := peers[spec.PublicKey]
	_, foundByDevice := wgFindByDeviceID(peers, spec.DeviceID)

	// --- determinar AllowedIPs ---
	allowedIPs := spec.AllowedIPs
	if allowedIPs == "" {
		if foundByKey && existingBlock.allowedIPs != "" {
			// reusar la IP del peer existente para idempotencia
			allowedIPs = existingBlock.allowedIPs
		} else {
			ip, err := wgNextFreeIP(subnetCIDR, peers)
			if err != nil {
				return WGPeerResult{}, fmt.Errorf("wg_peer_add: assign ip: %w", err)
			}
			allowedIPs = ip + "/32"
		}
	}

	// extraer IP pura para el resultado
	assignedIP := allowedIPs
	if idx := strings.Index(allowedIPs, "/"); idx >= 0 {
		assignedIP = allowedIPs[:idx]
	}

	if foundByKey {
		// misma PublicKey ya está — verificar si la config coincide
		if existingBlock.allowedIPs == allowedIPs &&
			existingBlock.presharedKey == spec.PresharedKey {
			return WGPeerResult{
				DeviceID:   spec.DeviceID,
				AssignedIP: assignedIP,
				ConfigPath: configPath,
				Status:     "already-present",
			}, nil
		}
		status = "reconfigured"
	} else if foundByDevice {
		// DeviceID existe con otra PublicKey → reconfigured (replace)
		status = "reconfigured"
	}

	// --- construir nueva config ---
	newContent, err := wgRebuildConfig(content, spec, allowedIPs, status)
	if err != nil {
		return WGPeerResult{}, fmt.Errorf("wg_peer_add: rebuild config: %w", err)
	}

	// --- backup ---
	backupPath := configPath + ".bak"
	if len(existing) > 0 {
		if err := os.WriteFile(backupPath, existing, 0600); err != nil {
			return WGPeerResult{}, fmt.Errorf("wg_peer_add: backup: %w", err)
		}
	}

	// --- escritura atómica ---
	tmpPath := configPath + ".tmp"
	if err := os.WriteFile(tmpPath, []byte(newContent), 0600); err != nil {
		return WGPeerResult{}, fmt.Errorf("wg_peer_add: write tmp: %w", err)
	}
	if err := os.Rename(tmpPath, configPath); err != nil {
		_ = os.Remove(tmpPath)
		return WGPeerResult{}, fmt.Errorf("wg_peer_add: rename: %w", err)
	}
	_ = os.Chmod(configPath, 0600)

	// --- syncconf ---
	iface := wgIfaceFromPath(configPath)
	if iface != "" {
		if err := wgSyncConfFn(iface, configPath); err != nil {
			// restaurar backup
			if len(existing) > 0 {
				_ = os.WriteFile(configPath, existing, 0600)
			}
			return WGPeerResult{}, fmt.Errorf("wg_peer_add: syncconf: %w", err)
		}
	}

	return WGPeerResult{
		DeviceID:   spec.DeviceID,
		AssignedIP: assignedIP,
		ConfigPath: configPath,
		Status:     status,
	}, nil
}

// --- helpers internos ---

type wgPeerBlock struct {
	deviceID     string
	publicKey    string
	presharedKey string
	allowedIPs   string
	rawLines     []string // líneas originales del bloque incluyendo comentario # DeviceID
}

// wgParsePeers extrae todos los bloques [Peer] indexados por PublicKey.
func wgParsePeers(content string) (map[string]*wgPeerBlock, error) {
	peers := map[string]*wgPeerBlock{}
	scanner := bufio.NewScanner(strings.NewReader(content))

	var cur *wgPeerBlock
	var pendingDeviceID string

	for scanner.Scan() {
		line := scanner.Text()
		trimmed := strings.TrimSpace(line)

		// comentario de tracking: # DeviceID: <id>
		if strings.HasPrefix(trimmed, "# DeviceID:") {
			pendingDeviceID = strings.TrimSpace(strings.TrimPrefix(trimmed, "# DeviceID:"))
			if cur != nil {
				cur.rawLines = append(cur.rawLines, line)
			}
			continue
		}

		if trimmed == "[Peer]" {
			cur = &wgPeerBlock{deviceID: pendingDeviceID}
			cur.rawLines = append(cur.rawLines, line)
			pendingDeviceID = ""
			continue
		}

		if cur != nil {
			if trimmed == "" || strings.HasPrefix(trimmed, "[") {
				// fin del bloque
				if cur.publicKey != "" {
					peers[cur.publicKey] = cur
				}
				cur = nil
				if strings.HasPrefix(trimmed, "[") {
					// nueva sección, no es [Peer]
				}
				continue
			}
			cur.rawLines = append(cur.rawLines, line)
			if k, v, ok := wgKV(trimmed); ok {
				switch k {
				case "PublicKey":
					cur.publicKey = v
				case "PresharedKey":
					cur.presharedKey = v
				case "AllowedIPs":
					cur.allowedIPs = v
				}
			}
		}
	}
	if cur != nil && cur.publicKey != "" {
		peers[cur.publicKey] = cur
	}
	return peers, scanner.Err()
}

// wgFindByDeviceID busca un peer por DeviceID.
func wgFindByDeviceID(peers map[string]*wgPeerBlock, deviceID string) (*wgPeerBlock, bool) {
	for _, p := range peers {
		if p.deviceID == deviceID {
			return p, true
		}
	}
	return nil, false
}

// wgNextFreeIP encuentra la primera IP libre en subnetCIDR (excluyendo .1 del hub).
func wgNextFreeIP(subnetCIDR string, peers map[string]*wgPeerBlock) (string, error) {
	ip, ipNet, err := net.ParseCIDR(subnetCIDR)
	if err != nil {
		return "", fmt.Errorf("invalid subnetCIDR %q: %w", subnetCIDR, err)
	}
	_ = ip

	// recopilar IPs ya usadas
	used := map[string]bool{}
	for _, p := range peers {
		cidr := p.allowedIPs
		if idx := strings.Index(cidr, "/"); idx >= 0 {
			used[cidr[:idx]] = true
		}
	}

	// iterar desde .2 (hub es .1)
	for candidate := wgIncrIP(ipNet.IP); ipNet.Contains(candidate); candidate = wgIncrIP(candidate) {
		s := candidate.String()
		// saltar la dirección de red (.0) y broadcast
		if s == ipNet.IP.String() {
			continue
		}
		// saltar hub (.1)
		hubIP := wgIncrIP(ipNet.IP)
		if s == hubIP.String() {
			// esta es .1 (hub), saltar
			continue
		}
		if !used[s] {
			return s, nil
		}
	}
	return "", fmt.Errorf("no free IPs in %s", subnetCIDR)
}

// wgIncrIP incrementa una IP en 1.
func wgIncrIP(ip net.IP) net.IP {
	ip = ip.To4()
	if ip == nil {
		return nil
	}
	result := make(net.IP, 4)
	copy(result, ip)
	for i := 3; i >= 0; i-- {
		result[i]++
		if result[i] != 0 {
			break
		}
	}
	return result
}

// wgRebuildConfig reconstruye el contenido del config con el peer añadido/reemplazado.
func wgRebuildConfig(content string, spec WGPeerSpec, allowedIPs, status string) (string, error) {
	// construir nuevo bloque
	var newBlock strings.Builder
	newBlock.WriteString(fmt.Sprintf("# DeviceID: %s\n", spec.DeviceID))
	newBlock.WriteString("[Peer]\n")
	newBlock.WriteString(fmt.Sprintf("PublicKey = %s\n", spec.PublicKey))
	if spec.PresharedKey != "" {
		newBlock.WriteString(fmt.Sprintf("PresharedKey = %s\n", spec.PresharedKey))
	}
	newBlock.WriteString(fmt.Sprintf("AllowedIPs = %s\n", allowedIPs))

	if status == "added" {
		// simplemente añadir al final
		result := content
		if !strings.HasSuffix(result, "\n") && len(result) > 0 {
			result += "\n"
		}
		result += "\n" + newBlock.String()
		return result, nil
	}

	// reconfigured: reemplazar el bloque existente (buscar por DeviceID o PublicKey)
	result, err := wgReplaceBlock(content, spec, newBlock.String())
	if err != nil {
		return "", err
	}
	return result, nil
}

// wgReplaceBlock reemplaza el bloque del peer identificado por DeviceID o PublicKey.
// Estrategia: parsear el config en segmentos (pre-bloque / bloque-target / post-bloque)
// y reconstruir sustituyendo solo el bloque target.
func wgReplaceBlock(content string, spec WGPeerSpec, newBlock string) (string, error) {
	type segment struct {
		isPeer  bool
		lines   []string // incluye comentario # DeviceID si lo había
		pk      string
		did     string
	}

	var segments []segment
	scanner := bufio.NewScanner(strings.NewReader(content))

	var cur *segment
	var pendingComment string

	flush := func() {
		if cur != nil {
			segments = append(segments, *cur)
			cur = nil
		}
	}

	for scanner.Scan() {
		line := scanner.Text()
		trimmed := strings.TrimSpace(line)

		if strings.HasPrefix(trimmed, "# DeviceID:") {
			pendingComment = line
			continue
		}

		if trimmed == "[Peer]" {
			flush()
			seg := segment{isPeer: true}
			if pendingComment != "" {
				seg.lines = append(seg.lines, pendingComment)
				seg.did = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(pendingComment), "# DeviceID:"))
				pendingComment = ""
			}
			seg.lines = append(seg.lines, line)
			cur = &seg
			continue
		}

		if cur != nil && cur.isPeer {
			if trimmed == "" || strings.HasPrefix(trimmed, "[") {
				flush()
				if pendingComment != "" {
					segments = append(segments, segment{lines: []string{pendingComment}})
					pendingComment = ""
				}
				if trimmed != "" {
					cur = &segment{lines: []string{line}}
				}
				continue
			}
			cur.lines = append(cur.lines, line)
			if k, v, ok := wgKV(trimmed); ok && k == "PublicKey" {
				cur.pk = v
			}
			continue
		}

		// línea fuera de bloque peer
		if pendingComment != "" {
			if cur == nil {
				cur = &segment{}
			}
			cur.lines = append(cur.lines, pendingComment)
			pendingComment = ""
		}
		if cur == nil {
			cur = &segment{}
		}
		cur.lines = append(cur.lines, line)
	}
	flush()
	if err := scanner.Err(); err != nil {
		return "", err
	}

	// reconstruir sustituyendo el target
	var out strings.Builder
	replaced := false
	for _, seg := range segments {
		if seg.isPeer && !replaced {
			isTarget := (seg.pk == spec.PublicKey) ||
				(spec.DeviceID != "" && seg.did == spec.DeviceID)
			if isTarget {
				out.WriteString("\n" + newBlock)
				replaced = true
				continue
			}
		}
		for _, l := range seg.lines {
			out.WriteString(l + "\n")
		}
	}

	if !replaced {
		result := out.String()
		if !strings.HasSuffix(result, "\n") && len(result) > 0 {
			result += "\n"
		}
		result += "\n" + newBlock
		return result, nil
	}

	return out.String(), nil
}

// wgKV parsea "Key = Value" devolviendo (key, value, true) o ("", "", false).
func wgKV(line string) (string, string, bool) {
	idx := strings.IndexByte(line, '=')
	if idx < 0 {
		return "", "", false
	}
	return strings.TrimSpace(line[:idx]), strings.TrimSpace(line[idx+1:]), true
}
