package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
)

// PortAllocator отслеживает порты, выделенные в текущем запуске программы.
// Заменяет глобальную переменную allocatedPortsInCurrentRun.
type PortAllocator struct {
	allocated map[int]bool
}

// NewPortAllocator создаёт новый аллокатор портов
func NewPortAllocator() *PortAllocator {
	return &PortAllocator{allocated: make(map[int]bool)}
}

// markAsUsed помечает порт как занятый в текущем запуске.
// Используется при загрузке сохранённых портов из .ports.json,
// чтобы другие клиенты/контейнеры не получили тот же порт.
func (pa *PortAllocator) markAsUsed(port int) {
	pa.allocated[port] = true
}

// allocatePortsForResources выделяет порты для всех ресурсов и контейнеров.
// excludePorts — порты, которые нужно исключить из проверки занятости (может быть nil).
// Возвращает map[l7ResourceID][]PortMapping где каждый элемент массива — порты для одного контейнера.
func (pa *PortAllocator) allocatePortsForResources(resourcesData []ResourceData, containersCount int, excludePorts map[int]bool) (map[int][]PortMapping, error) {
	// Получаем список занятых портов в системе
	usedPorts, err := getUsedPorts()
	if err != nil {
		return nil, fmt.Errorf("ошибка получения занятых портов: %w", err)
	}

	// Исключаем порты клиента (если передано) для переиспользования
	for port := range excludePorts {
		delete(usedPorts, port)
	}

	// Объединяем с портами из текущего запуска
	for port := range pa.allocated {
		usedPorts[port] = true
	}

	resourcePortMap := make(map[int][]PortMapping)
	currentHTTPPort := HTTPPortStart
	currentHTTPSPort := HTTPSPortStart

	for _, res := range resourcesData {
		var portMappingsForResource []PortMapping

		httpPortsNeeded := getInputHTTPPorts(res.AppsSettings)
		httpsPortsNeeded := getInputHTTPSPorts(res.AppsSettings)

		for containerNum := 0; containerNum < containersCount; containerNum++ {
			portMapping := PortMapping{
				HTTPPorts:  make(map[int]int),
				HTTPSPorts: make(map[int]int),
			}

			// Выделяем Docker порты для HTTP
			for _, customPort := range httpPortsNeeded {
				for usedPorts[currentHTTPPort] && currentHTTPPort <= HTTPPortEnd {
					currentHTTPPort++
				}
				if currentHTTPPort > HTTPPortEnd {
					return nil, fmt.Errorf("не хватает свободных HTTP портов (диапазон %d-%d исчерпан)", HTTPPortStart, HTTPPortEnd)
				}

				portMapping.HTTPPorts[customPort] = currentHTTPPort
				usedPorts[currentHTTPPort] = true
				pa.allocated[currentHTTPPort] = true
				currentHTTPPort++
			}

			// Выделяем Docker порты для HTTPS
			for _, customPort := range httpsPortsNeeded {
				for usedPorts[currentHTTPSPort] && currentHTTPSPort <= HTTPSPortEnd {
					currentHTTPSPort++
				}
				if currentHTTPSPort > HTTPSPortEnd {
					return nil, fmt.Errorf("не хватает свободных HTTPS портов (диапазон %d-%d исчерпан)", HTTPSPortStart, HTTPSPortEnd)
				}

				portMapping.HTTPSPorts[customPort] = currentHTTPSPort
				usedPorts[currentHTTPSPort] = true
				pa.allocated[currentHTTPSPort] = true
				currentHTTPSPort++
			}

			portMappingsForResource = append(portMappingsForResource, portMapping)
		}

		resourcePortMap[res.L7ResourceID] = portMappingsForResource

		// Логирование
		totalHTTPPorts := len(httpPortsNeeded) * containersCount
		totalHTTPSPorts := len(httpsPortsNeeded) * containersCount
		log.Printf("  → Ресурс %d: выделено %d HTTP портов и %d HTTPS портов (%d контейнеров × %d+%d портов)",
			res.L7ResourceID, totalHTTPPorts, totalHTTPSPorts, containersCount, len(httpPortsNeeded), len(httpsPortsNeeded))
	}

	return resourcePortMap, nil
}

// getUsedPorts возвращает map занятых портов
func getUsedPorts() (map[int]bool, error) {
	usedPorts := make(map[int]bool)

	cmd := exec.Command("ss", "-tuln")
	output, err := cmd.Output()
	if err != nil {
		return usedPorts, fmt.Errorf("ошибка выполнения ss: %w", err)
	}

	lines := strings.Split(string(output), "\n")
	for _, line := range lines {
		fields := strings.Fields(line)
		if len(fields) < 5 {
			continue
		}

		localAddr := fields[4]
		parts := strings.Split(localAddr, ":")
		if len(parts) < 2 {
			continue
		}

		port := 0
		fmt.Sscanf(parts[len(parts)-1], "%d", &port)
		if (port >= HTTPPortStart && port <= HTTPPortEnd) || (port >= HTTPSPortStart && port <= HTTPSPortEnd) {
			usedPorts[port] = true
		}
	}

	return usedPorts, nil
}

// saveContainerPorts сохраняет порты контейнера в файл .ports.json
func saveContainerPorts(containerDir string, containerNum int, resources []ResourceData, portMappings []PortMapping) error {
	portsFile := filepath.Join(containerDir, ".ports.json")

	savedPorts := SavedContainerPorts{
		ContainerNum: containerNum,
		Resources:    make([]SavedResourcePorts, 0),
	}

	for idx, res := range resources {
		if idx >= len(portMappings) {
			continue
		}

		savedPorts.Resources = append(savedPorts.Resources, SavedResourcePorts{
			L7ResourceID: res.L7ResourceID,
			HTTPPorts:    portMappings[idx].HTTPPorts,
			HTTPSPorts:   portMappings[idx].HTTPSPorts,
		})
	}

	data, err := json.MarshalIndent(savedPorts, "", "  ")
	if err != nil {
		return fmt.Errorf("ошибка сериализации портов: %w", err)
	}

	if err := os.WriteFile(portsFile, data, 0644); err != nil {
		return fmt.Errorf("ошибка записи файла портов: %w", err)
	}

	return nil
}

// loadContainerPorts загружает порты контейнера из файла .ports.json
func loadContainerPorts(containerDir string, resources []ResourceData) ([]PortMapping, bool) {
	portsFile := filepath.Join(containerDir, ".ports.json")

	data, err := os.ReadFile(portsFile)
	if err != nil {
		return nil, false
	}

	var savedPorts SavedContainerPorts
	if err := json.Unmarshal(data, &savedPorts); err != nil {
		log.Printf("  ⚠ Ошибка парсинга %s: %v", portsFile, err)
		return nil, false
	}

	if len(savedPorts.Resources) != len(resources) {
		log.Printf("  ⚠ Количество ресурсов изменилось (было %d, стало %d), перевыделяем порты",
			len(savedPorts.Resources), len(resources))
		return nil, false
	}

	for idx, savedRes := range savedPorts.Resources {
		if savedRes.L7ResourceID != resources[idx].L7ResourceID {
			log.Printf("  ⚠ Порядок ресурсов изменился (позиция %d: было %d, стало %d), перевыделяем порты",
				idx, savedRes.L7ResourceID, resources[idx].L7ResourceID)
			return nil, false
		}
	}

	// Проверка: изменились ли кастомные порты?
	for idx, savedRes := range savedPorts.Resources {
		currentRes := resources[idx]

		// Получаем текущие кастомные порты из БД
		currentHTTPPorts := getInputHTTPPorts(currentRes.AppsSettings)
		currentHTTPSPorts := getInputHTTPSPorts(currentRes.AppsSettings)

		// Проверяем HTTP порты
		if !portsMatch(savedRes.HTTPPorts, currentHTTPPorts) {
			log.Printf("  ⚠ Кастомные HTTP порты изменились для ресурса %d, перевыделяем порты",
				currentRes.L7ResourceID)
			log.Printf("    Было: %v, Стало: %v", getMapKeysInt(savedRes.HTTPPorts), currentHTTPPorts)
			return nil, false
		}

		// Проверяем HTTPS порты
		if !portsMatch(savedRes.HTTPSPorts, currentHTTPSPorts) {
			log.Printf("  ⚠ Кастомные HTTPS порты изменились для ресурса %d, перевыделяем порты",
				currentRes.L7ResourceID)
			log.Printf("    Было: %v, Стало: %v", getMapKeysInt(savedRes.HTTPSPorts), currentHTTPSPorts)
			return nil, false
		}
	}

	portMappings := make([]PortMapping, len(savedPorts.Resources))
	for idx, savedRes := range savedPorts.Resources {
		portMappings[idx] = PortMapping{
			HTTPPorts:  savedRes.HTTPPorts,
			HTTPSPorts: savedRes.HTTPSPorts,
		}
	}

	return portMappings, true
}

// portsMatch проверяет совпадают ли кастомные порты в сохраненном маппинге
// с текущими кастомными портами из БД
func portsMatch(savedMapping map[int]int, currentCustomPorts []int) bool {
	// Проверяем что количество совпадает
	if len(savedMapping) != len(currentCustomPorts) {
		return false
	}

	// Проверяем что все текущие кастомные порты есть в сохраненном маппинге
	for _, customPort := range currentCustomPorts {
		if _, exists := savedMapping[customPort]; !exists {
			return false
		}
	}

	return true
}

// getMapKeysInt возвращает ключи из map[int]int в виде отсортированного слайса
func getMapKeysInt(m map[int]int) []int {
	keys := make([]int, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Ints(keys)
	return keys
}
