// Copyright 2025 Microsoft Corporation
//
// 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 run

import (
	"compress/gzip"
	"context"
	"fmt"
	"maps"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/spf13/cobra"

	"sigs.k8s.io/yaml"

	"github.com/Azure/ARO-HCP/tooling/templatize/cmd/entrypoint/entrypointutils"
	"github.com/Azure/ARO-HCP/tooling/templatize/pkg/pipeline"
)

func DefaultOptions() *RawOptions {
	return &RawOptions{
		RawOptions:               entrypointutils.DefaultOptions(),
		DeploymentTimeoutSeconds: pipeline.DefaultDeploymentTimeoutSeconds,
	}
}

func BindOptions(opts *RawOptions, cmd *cobra.Command) error {
	if err := entrypointutils.BindOptions(opts.RawOptions, cmd); err != nil {
		return err
	}

	cmd.Flags().StringVar(&opts.TimingOutputFile, "timing-output", opts.TimingOutputFile, "Path to the file where timing outputs will be written.")
	cmd.Flags().StringVar(&opts.JUnitOutputFile, "junit-output", opts.JUnitOutputFile, "If provided, jUnit outputs for pipeline steps will be written to this file.")
	cmd.Flags().StringVar(&opts.ConfigOutputFile, "config-output", opts.ConfigOutputFile, "If provided, the rendered configuration will be written to this file. Supports .gz extension for gzip compression.")

	cmd.Flags().BoolVar(&opts.Persist, "persist-tag", opts.Persist, "toggle if persist tag should be set")
	cmd.Flags().IntVar(&opts.DeploymentTimeoutSeconds, "deployment-timeout-seconds", opts.DeploymentTimeoutSeconds, "Timeout in Seconds to wait for previous deployments of the pipeline to finish")
	cmd.Flags().BoolVar(&opts.AbortIfRegionalExist, "abort-if-regional-exist", opts.AbortIfRegionalExist, "Abort deployment if regional resource groups already exist (concurrent execution prevention)")
	cmd.Flags().BoolVar(&opts.SkipBicepparamValidation, "skip-bicepparam-validation", opts.SkipBicepparamValidation, "Skip validation of bicepparam templates for simple field access.")

	return nil
}

type RawOptions struct {
	*entrypointutils.RawOptions

	Persist                  bool
	DeploymentTimeoutSeconds int
	AbortIfRegionalExist     bool
	SkipBicepparamValidation bool

	TimingOutputFile string
	JUnitOutputFile  string
	ConfigOutputFile string
}

// validatedOptions is a private wrapper that enforces a call of Validate() before Complete() can be invoked.
type validatedOptions struct {
	*RawOptions
	*entrypointutils.ValidatedOptions
}

type ValidatedOptions struct {
	// Embed a private pointer that cannot be instantiated outside of this package.
	*validatedOptions
}

// completedOptions is a private wrapper that enforces a call of Complete() before config generation can be invoked.
type completedOptions struct {
	*entrypointutils.Options

	NoPersist                bool
	DeploymentTimeoutSeconds int
	AbortIfRegionalExist     bool
	SkipBicepparamValidation bool

	TimingOutputFile string
	JUnitOutputFile  string
	ConfigOutputFile string
}

type Options struct {
	// Embed a private pointer that cannot be instantiated outside of this package.
	*completedOptions
}

func (o *RawOptions) Validate(ctx context.Context) (*ValidatedOptions, error) {
	validated, err := o.RawOptions.Validate(ctx)
	if err != nil {
		return nil, err
	}

	return &ValidatedOptions{
		validatedOptions: &validatedOptions{
			RawOptions:       o,
			ValidatedOptions: validated,
		},
	}, nil
}

func (o *ValidatedOptions) Complete(ctx context.Context) (*Options, error) {
	completed, err := o.ValidatedOptions.Complete(ctx)
	if err != nil {
		return nil, err
	}

	return &Options{
		completedOptions: &completedOptions{
			Options: completed,

			NoPersist:                !o.Persist,
			DeploymentTimeoutSeconds: o.DeploymentTimeoutSeconds,
			AbortIfRegionalExist:     o.AbortIfRegionalExist,
			SkipBicepparamValidation: o.SkipBicepparamValidation,

			TimingOutputFile: o.TimingOutputFile,
			JUnitOutputFile:  o.JUnitOutputFile,
			ConfigOutputFile: o.ConfigOutputFile,
		},
	}, nil
}

func (o *Options) Run(ctx context.Context) error {
	if o.ConfigOutputFile != "" {
		if err := o.writeConfigOutput(); err != nil {
			return fmt.Errorf("failed to write config output: %w", err)
		}
	}

	allPipelines := slices.Collect(maps.Values(o.Pipelines))
	for _, stampPipelines := range o.StampPipelines {
		allPipelines = append(allPipelines, slices.Collect(maps.Values(stampPipelines))...)
	}

	subscriptionIDs, err := pipeline.ResolvePipelineSubscriptions(ctx, pipeline.LookupSubscriptionID(o.Subscriptions), allPipelines...)
	if err != nil {
		return fmt.Errorf("failed to resolve pipeline subscriptions: %w", err)
	}

	subscriptionIdToAzureConfigDirectory, err := pipeline.GetAllRequiredAzureClients(ctx, subscriptionIDs)
	if err != nil {
		return fmt.Errorf("failed to get all required Azure clients: %w", err)
	}
	defer func() {
		for _, azureConfigDir := range subscriptionIdToAzureConfigDirectory {
			os.RemoveAll(azureConfigDir)
		}
	}()

	// Extract target resource group names from config for precheck
	var regionRGNames []string
	if o.AbortIfRegionalExist {
		regionRGNames = entrypointutils.RegionalResourceGroupNames(o.StampConfigs)
	}

	runOpts := &pipeline.PipelineRunOptions{
		BaseRunOptions: pipeline.BaseRunOptions{
			Cloud:                                o.Cloud,
			Configuration:                        o.Config,
			NoPersist:                            o.NoPersist,
			DeploymentTimeoutSeconds:             o.DeploymentTimeoutSeconds,
			StepCacheDir:                         o.StepCacheDir,
			BicepClient:                          o.BicepClient,
			SkipBicepparamValidation:             o.SkipBicepparamValidation,
			SubscriptionIdToAzureConfigDirectory: subscriptionIdToAzureConfigDirectory,
		},
		TopoDirLookupFunc:     o.Topo.GetTopologyDirForServiceGroup,
		Region:                o.Region,
		Environment:           o.DeployEnv,
		SubsciptionLookupFunc: pipeline.LookupSubscriptionID(o.Subscriptions),
		Concurrency:           o.Concurrency,
		TimingOutputFile:      o.TimingOutputFile,
		JUnitOutputFile:       o.JUnitOutputFile,
		AbortIfRegionalExist:  o.AbortIfRegionalExist,
		RegionRGNames:         regionRGNames,
		StampConfigs:          o.StampConfigs,
		StampPipelines:        o.StampPipelines,
	}

	if o.Entrypoint != nil {
		_, err := pipeline.RunEntrypoint(o.Topo, o.Entrypoint, o.Pipelines, ctx, runOpts, pipeline.RunStep)
		return err
	}

	_, err = pipeline.RunStampedPipeline(o.Service, o.Pipelines[o.Service.ServiceGroup], ctx, runOpts, pipeline.RunStep)
	return err
}

func (o *Options) writeConfigOutput() error {
	data, err := yaml.Marshal(o.Config)
	if err != nil {
		return fmt.Errorf("failed to marshal config: %w", err)
	}

	if err := os.MkdirAll(filepath.Dir(o.ConfigOutputFile), 0755); err != nil {
		return fmt.Errorf("failed to create output directory: %w", err)
	}

	f, err := os.Create(o.ConfigOutputFile)
	if err != nil {
		return fmt.Errorf("failed to create output file: %w", err)
	}
	defer f.Close()

	if strings.HasSuffix(o.ConfigOutputFile, ".gz") {
		gw, err := gzip.NewWriterLevel(f, gzip.BestCompression)
		if err != nil {
			return fmt.Errorf("failed to create gzip writer: %w", err)
		}
		defer gw.Close()
		_, err = gw.Write(data)
		return err
	}

	_, err = f.Write(data)
	return err
}
