// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package ports

import (
	"context"
	"fmt"
	"reflect"
	"regexp"
	"strconv"

	gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
	"github.com/gitpod-io/gitpod/supervisor/pkg/config"
)

const NON_CONFIGED_BASIC_SCORE = 100000

// RangeConfig is a port range config.
type RangeConfig struct {
	gitpod.PortsItems
	Start uint32
	End   uint32
	Sort  uint32
}

// SortConfig is a port with a sort field
type SortConfig struct {
	gitpod.PortConfig
	Sort uint32
}

// Configs provides access to port configurations.
type Configs struct {
	instancePortConfigs  map[uint32]*SortConfig
	instanceRangeConfigs []*RangeConfig
}

// ForEach iterates over all configured ports.
func (configs *Configs) ForEach(callback func(port uint32, config *SortConfig)) {
	if configs == nil {
		return
	}
	visited := make(map[uint32]struct{})
	for port, config := range configs.instancePortConfigs {
		_, exists := visited[port]
		if exists {
			continue
		}
		visited[port] = struct{}{}
		callback(port, config)
	}
}

// ConfigKind indicates a type of config.
type ConfigKind uint8

var (
	// PortConfigKind is a port based config type.
	PortConfigKind ConfigKind = 0
	// RangeConfigKind is a range based config type.
	RangeConfigKind ConfigKind = 1
)

// Get returns the config for the give port.
func (configs *Configs) Get(port uint32) (*SortConfig, ConfigKind, bool) {
	if configs == nil {
		return nil, PortConfigKind, false
	}
	config, exists := configs.instancePortConfigs[port]
	if exists {
		return config, PortConfigKind, true
	}
	for _, rangeConfig := range configs.instanceRangeConfigs {
		if rangeConfig.Start <= port && port <= rangeConfig.End {
			return &SortConfig{
				PortConfig: gitpod.PortConfig{
					Port:        float64(port),
					OnOpen:      rangeConfig.OnOpen,
					Visibility:  rangeConfig.Visibility,
					Description: rangeConfig.Description,
					Name:        rangeConfig.Name,
				},
				Sort: rangeConfig.Sort,
			}, RangeConfigKind, true
		}
	}
	return nil, PortConfigKind, false
}

// ConfigInterace allows to watch port configurations.
type ConfigInterace interface {
	// Observe provides channels triggered whenever the port configurations are changed.
	Observe(ctx context.Context) (<-chan *Configs, <-chan error)
}

// ConfigService allows to watch port configurations.
type ConfigService struct {
	workspaceID   string
	configService config.ConfigInterface
}

// NewConfigService creates a new instance of ConfigService.
func NewConfigService(workspaceID string, configService config.ConfigInterface) *ConfigService {
	return &ConfigService{
		workspaceID:   workspaceID,
		configService: configService,
	}
}

// Observe provides channels triggered whenever the port configurations are changed.
func (service *ConfigService) Observe(ctx context.Context) (<-chan *Configs, <-chan error) {
	updatesChan := make(chan *Configs)
	errorsChan := make(chan error, 1)

	go func() {
		defer close(updatesChan)
		defer close(errorsChan)

		configs := service.configService.Observe(ctx)

		current := &Configs{}

		for {
			select {
			case <-ctx.Done():
				return
			case config, ok := <-configs:
				if !ok {
					return
				}
				changed := service.update(config, current)
				if !changed {
					continue
				}
				updatesChan <- &Configs{
					instancePortConfigs:  current.instancePortConfigs,
					instanceRangeConfigs: current.instanceRangeConfigs,
				}
			}
		}
	}()
	return updatesChan, errorsChan
}

func (service *ConfigService) update(config *gitpod.GitpodConfig, current *Configs) bool {
	currentPortConfigs, currentRangeConfigs := current.instancePortConfigs, current.instanceRangeConfigs
	var ports []*gitpod.PortsItems
	if config != nil {
		ports = config.Ports
	}
	portConfigs, rangeConfigs := parseInstanceConfigs(ports)
	current.instancePortConfigs = portConfigs
	current.instanceRangeConfigs = rangeConfigs
	return !reflect.DeepEqual(currentPortConfigs, portConfigs) || !reflect.DeepEqual(currentRangeConfigs, rangeConfigs)
}

var portRangeRegexp = regexp.MustCompile(`^(\d+)[-:](\d+)$`)

func parseInstanceConfigs(ports []*gitpod.PortsItems) (portConfigs map[uint32]*SortConfig, rangeConfigs []*RangeConfig) {
	for index, config := range ports {
		if config == nil {
			continue
		}

		rawPort := fmt.Sprintf("%v", config.Port)
		Port, err := strconv.ParseUint(rawPort, 10, 16)
		if err == nil {
			if portConfigs == nil {
				portConfigs = make(map[uint32]*SortConfig)
			}
			port := uint32(Port)
			_, exists := portConfigs[port]
			if !exists {
				portConfigs[port] = &SortConfig{
					PortConfig: gitpod.PortConfig{
						OnOpen:      config.OnOpen,
						Port:        float64(Port),
						Visibility:  config.Visibility,
						Description: config.Description,
						Name:        config.Name,
					},
					Sort: uint32(index),
				}
			}
			continue
		}
		matches := portRangeRegexp.FindStringSubmatch(rawPort)
		if len(matches) != 3 {
			continue
		}
		start, err := strconv.Atoi(matches[1])
		if err != nil {
			continue
		}
		end, err := strconv.Atoi(matches[2])
		if err != nil || start >= end {
			continue
		}
		rangeConfigs = append(rangeConfigs, &RangeConfig{
			PortsItems: *config,
			Start:      uint32(start),
			End:        uint32(end),
			Sort:       uint32(index),
		})
	}
	return portConfigs, rangeConfigs
}
