// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package compiler

import (
	"fmt"
	"maps"
	"path"
	"strconv"
	"strings"

	"github.com/oklog/ulid/v2"

	backend_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
	"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
	"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/compiler/settings"
	yaml_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types"
	"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils"
)

func (c *Compiler) createProcess(container *yaml_types.Container, stepType backend_types.StepType) (*backend_types.Step, error) {
	var (
		uuid = ulid.Make()

		detached   bool
		workingDir string

		workspace   = fmt.Sprintf("%s_default:%s", c.prefix, c.base)
		privileged  = container.Privileged
		networkMode = container.NetworkMode
		// network    = container.Network
	)

	networks := []backend_types.Conn{
		{
			Name:    fmt.Sprintf("%s_default", c.prefix),
			Aliases: []string{container.Name},
		},
	}
	for _, network := range c.networks {
		networks = append(networks, backend_types.Conn{
			Name: network,
		})
	}

	extraHosts := make([]backend_types.HostAlias, len(container.ExtraHosts))
	for i, extraHost := range container.ExtraHosts {
		name, ip, ok := strings.Cut(extraHost, ":")
		if !ok {
			return nil, &ErrExtraHostFormat{host: extraHost}
		}
		extraHosts[i].Name = name
		extraHosts[i].IP = ip
	}

	var volumes []string
	if !c.local {
		volumes = append(volumes, workspace)
	}
	volumes = append(volumes, c.volumes...)
	for _, volume := range container.Volumes.Volumes {
		volumes = append(volumes, volume.String())
	}

	// append default environment variables
	environment := map[string]string{}
	maps.Copy(environment, c.env)

	environment["CI_WORKSPACE"] = path.Join(c.base, c.path)

	if stepType == backend_types.StepTypeService || container.Detached {
		detached = true
	}

	if !detached || len(container.Commands) != 0 {
		workingDir = c.stepWorkingDir(container)
	}

	getSecretValue := func(name string) (string, error) {
		name = strings.ToLower(name)
		secret, ok := c.secrets[name]
		if !ok {
			return "", fmt.Errorf("secret %q not found", name)
		}

		event := c.metadata.Curr.Event
		err := secret.Available(event, container)
		if err != nil {
			return "", err
		}

		return secret.Value, nil
	}

	// TODO: why don't we pass secrets to detached steps?
	if !detached {
		if err := settings.ParamsToEnv(container.Settings, environment, "PLUGIN_", true, getSecretValue); err != nil {
			return nil, err
		}
	}

	if err := settings.ParamsToEnv(container.Environment, environment, "", false, getSecretValue); err != nil {
		return nil, err
	}

	for _, requested := range container.Secrets.Secrets {
		secretValue, err := getSecretValue(requested.Source)
		if err != nil {
			return nil, err
		}

		environment[requested.Target] = secretValue
		// TODO deprecated, remove in 3.x
		environment[strings.ToUpper(requested.Target)] = secretValue
	}

	if utils.MatchImage(container.Image, c.escalated...) && container.IsPlugin() {
		privileged = true
	}

	authConfig := backend_types.Auth{}
	for _, registry := range c.registries {
		if utils.MatchHostname(container.Image, registry.Hostname) {
			authConfig.Username = registry.Username
			authConfig.Password = registry.Password
			break
		}
	}

	memSwapLimit := int64(container.MemSwapLimit)
	if c.reslimit.MemSwapLimit != 0 {
		memSwapLimit = c.reslimit.MemSwapLimit
	}
	memLimit := int64(container.MemLimit)
	if c.reslimit.MemLimit != 0 {
		memLimit = c.reslimit.MemLimit
	}
	shmSize := int64(container.ShmSize)
	if c.reslimit.ShmSize != 0 {
		shmSize = c.reslimit.ShmSize
	}
	cpuQuota := int64(container.CPUQuota)
	if c.reslimit.CPUQuota != 0 {
		cpuQuota = c.reslimit.CPUQuota
	}
	cpuShares := int64(container.CPUShares)
	if c.reslimit.CPUShares != 0 {
		cpuShares = c.reslimit.CPUShares
	}
	cpuSet := container.CPUSet
	if c.reslimit.CPUSet != "" {
		cpuSet = c.reslimit.CPUSet
	}

	var ports []backend_types.Port
	for _, portDef := range container.Ports {
		port, err := convertPort(portDef)
		if err != nil {
			return nil, err
		}
		ports = append(ports, port)
	}

	// at least one constraint contain status success, or all constraints have no status set
	onSuccess := container.When.IncludesStatusSuccess()
	// at least one constraint must include the status failure.
	onFailure := container.When.IncludesStatusFailure()

	failure := container.Failure
	if container.Failure == "" {
		failure = metadata.FailureFail
	}

	return &backend_types.Step{
		Name:           container.Name,
		UUID:           uuid.String(),
		Type:           stepType,
		Image:          container.Image,
		Pull:           container.Pull,
		Detached:       detached,
		Privileged:     privileged,
		WorkingDir:     workingDir,
		Environment:    environment,
		Commands:       container.Commands,
		Entrypoint:     container.Entrypoint,
		ExtraHosts:     extraHosts,
		Volumes:        volumes,
		Tmpfs:          container.Tmpfs,
		Devices:        container.Devices,
		Networks:       networks,
		DNS:            container.DNS,
		DNSSearch:      container.DNSSearch,
		MemSwapLimit:   memSwapLimit,
		MemLimit:       memLimit,
		ShmSize:        shmSize,
		CPUQuota:       cpuQuota,
		CPUShares:      cpuShares,
		CPUSet:         cpuSet,
		AuthConfig:     authConfig,
		OnSuccess:      onSuccess,
		OnFailure:      onFailure,
		Failure:        failure,
		NetworkMode:    networkMode,
		Ports:          ports,
		BackendOptions: container.BackendOptions,
	}, nil
}

func (c *Compiler) stepWorkingDir(container *yaml_types.Container) string {
	if path.IsAbs(container.Directory) {
		return container.Directory
	}
	return path.Join(c.base, c.path, container.Directory)
}

func convertPort(portDef string) (backend_types.Port, error) {
	var err error
	var port backend_types.Port

	number, protocol, _ := strings.Cut(portDef, "/")
	port.Protocol = protocol

	portNumber, err := strconv.ParseUint(number, 10, 16)
	if err != nil {
		return port, err
	}
	port.Number = uint16(portNumber)

	return port, nil
}