package osbuild2

import (
	"bytes"
	"encoding/json"
	"fmt"
	"regexp"
	"sort"
)

type OCIArchiveStageOptions struct {
	// The CPU architecture of the image
	Architecture string `json:"architecture"`

	// Resulting image filename
	Filename string `json:"filename"`

	// The execution parameters
	Config *OCIArchiveConfig `json:"config,omitempty"`
}

type OCIArchiveConfig struct {
	Cmd          []string          `json:"Cmd,omitempty"`
	Env          []string          `json:"Env,omitempty"`
	ExposedPorts []string          `json:"ExposedPorts,omitempty"`
	User         string            `json:"User,omitempty"`
	Labels       map[string]string `json:"Labels,omitempty"`
	StopSignal   string            `json:"StopSignal,omitempty"`
	Volumes      []string          `json:"Volumes,omitempty"`
	WorkingDir   string            `json:"WorkingDir,omitempty"`
}

func (OCIArchiveStageOptions) isStageOptions() {}

type OCIArchiveStageInputs struct {
	// Base layer for the container
	Base *OCIArchiveStageInput `json:"base"`
	// Additional layers in ascending order
	Layers []OCIArchiveStageInput `json:",omitempty"`
}

func (OCIArchiveStageInputs) isStageInputs() {}

type OCIArchiveStageInput struct {
	inputCommon
	References OCIArchiveStageReferences `json:"references"`
}

func (OCIArchiveStageInput) isStageInput() {}

type OCIArchiveStageReferences []string

func (OCIArchiveStageReferences) isReferences() {}

// A new OCIArchiveStage to to assemble an OCI image archive
func NewOCIArchiveStage(options *OCIArchiveStageOptions, inputs *OCIArchiveStageInputs) *Stage {
	return &Stage{
		Type:    "org.osbuild.oci-archive",
		Options: options,
		Inputs:  inputs,
	}
}

// Custom marshaller for OCIArchiveStageInputs, needed to generate keys of the
// form "layer.N", (where N = 1, 2, ...) for the Layers property
func (inputs *OCIArchiveStageInputs) MarshalJSON() ([]byte, error) {
	if inputs == nil {
		return json.Marshal(inputs)
	}

	layers := inputs.Layers
	inputsMap := make(map[string]OCIArchiveStageInput, len(layers)+1)
	if inputs.Base != nil {
		inputsMap["base"] = *inputs.Base
	}

	for idx, input := range layers {
		key := fmt.Sprintf("layer.%d", idx+1)
		inputsMap[key] = input
	}

	return json.Marshal(inputsMap)
}

// Get the sorted keys that match the pattern "layer.N" (for N > 0)
func layerKeys(layers map[string]OCIArchiveStageInput) ([]string, error) {
	keys := make([]string, 0, len(layers))
	for key := range layers {
		re := regexp.MustCompile(`layer\.[1-9]\d*`)
		if key == "base" {
			continue
		}
		if !re.MatchString(key) {
			return nil, fmt.Errorf("invalid key: %q", key)
		}
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys, nil
}

// Custom unmarshaller for OCIArchiveStageInputs, needed to handle keys of the
// form "layer.N", (where N = 1, 2, ...) for the Layers property
func (inputs *OCIArchiveStageInputs) UnmarshalJSON(data []byte) error {
	if len(data) == 0 {
		return nil
	}

	if inputs == nil {
		inputs = new(OCIArchiveStageInputs)
	}

	inputsMap := make(map[string]OCIArchiveStageInput)

	dec := json.NewDecoder(bytes.NewReader(data))
	dec.DisallowUnknownFields()

	if err := dec.Decode(&inputsMap); err != nil {
		return err
	}

	// "base" layer is required
	base, ok := inputsMap["base"]
	if !ok {
		return fmt.Errorf("missing required key \"base\"")
	}

	inputs.Base = &base
	keys, err := layerKeys(inputsMap)
	if err != nil {
		return err
	}
	inputs.Layers = make([]OCIArchiveStageInput, len(inputsMap)-1)
	for idx, key := range keys {
		inputs.Layers[idx] = inputsMap[key]
	}

	return nil
}
