// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// 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 main

import (
	"context"

	"github.com/sirupsen/logrus"
	cli "github.com/urfave/cli/v3"

	"github.com/projectcalico/calico/release/internal/defaults"
	"github.com/projectcalico/calico/release/internal/images"
	"github.com/projectcalico/calico/release/internal/operator"
	"github.com/projectcalico/calico/release/internal/operatorimages"
	"github.com/projectcalico/calico/release/internal/steps"
)

var imagesSubCommands = func(cfg *Config) []*cli.Command {
	return []*cli.Command{
		imagesBuildCommand(cfg),
		imagesPublishCommand(cfg),
		imagesCheckOperatorCommand(cfg),
	}
}

func imagesCommand(cfg *Config) *cli.Command {
	return &cli.Command{
		Name:     "images",
		Usage:    "Build and publish container images",
		Commands: imagesSubCommands(cfg),
	}
}

var (
	imagesBuildFlags  = []cli.Flag{registryFlag, archFlag, imageReleaseDirsFlag, hashreleaseFlag}
	imagesBuildAction = func(cfg *Config) func(ctx context.Context, c *cli.Command) error {
		return func(ctx context.Context, c *cli.Command) error {
			configureLogging("images-build.log")
			ver, err := releaseVersion(cfg, c)
			if err != nil {
				return err
			}
			return images.Build(
				cfg.RepoRootDir, ver.String(),
				images.NarrowVariants(images.BuildVariants, c.StringSlice(imageReleaseDirsFlag.Name)),
				images.WithRunner(commandRunner),
				images.WithRegistries(c.StringSlice(registryFlag.Name)...),
				images.WithArches(c.StringSlice(archFlag.Name)...),
				images.WithLogsDir(cfg.LogsDir),
			)
		}
	}
)

func imagesBuildCommand(cfg *Config) *cli.Command {
	return &cli.Command{
		Name:   "build",
		Usage:  "Build container images",
		Flags:  imagesBuildFlags,
		Action: imagesBuildAction(cfg),
	}
}

var (
	imagesPublishFlags = append([]cli.Flag{
		registryFlag, archFlag, localFlag, imageReleaseDirsFlag, hashreleaseFlag,
		fromRegistryFlag, fromTagFlag, skipDevImageRetagFlag, forceFlag,
	}, imageScanFlags...)
	imagesPublishAction = func(cfg *Config) func(ctx context.Context, c *cli.Command) error {
		return func(_ context.Context, c *cli.Command) error {
			configureLogging("images-publish.log")
			ver, err := releaseVersion(cfg, c)
			if err != nil {
				return err
			}
			// A narrowed run must scan only what it published.
			dirs := c.StringSlice(imageReleaseDirsFlag.Name)
			scanDirs := dirs
			if len(scanDirs) == 0 {
				scanDirs = images.ReleaseDirs()
			}
			scan, err := scanRequest(c, cfg, scanDirs, ver.PrimaryStream(), defaults.ProductCode)
			if err != nil {
				return err
			}
			records, err := recordsDir(cfg, c, oncePin(pinForPublish), ver.String())
			if err != nil {
				return err
			}
			published, w, err := publishRecord(records, images.PublishStep, !c.Bool(localFlag.Name))
			if err != nil {
				return err
			}
			var refs steps.RefRecorder
			if w != nil {
				refs = w
			}
			opts := []images.PublishOption{
				images.WithRunner(commandRunner),
				images.WithRegistries(c.StringSlice(registryFlag.Name)...),
				images.WithArches(c.StringSlice(archFlag.Name)...),
				images.WithLogsDir(cfg.LogsDir),
			}
			if from := c.String(fromRegistryFlag.Name); from != "" {
				opts = append(opts, images.WithRetag(from,
					c.String(fromTagFlag.Name), c.Bool(skipDevImageRetagFlag.Name)))
			}
			if scan != nil {
				opts = append(opts, images.WithScan(scan))
			}
			if refs != nil {
				opts = append(opts, images.WithRecord(refs))
			}
			if len(published) > 0 {
				opts = append(opts, images.WithResume(published, c.Bool(forceFlag.Name)))
			}

			return images.Publish(
				cfg.RepoRootDir, ver.String(),
				images.NarrowVariants(images.PublishVariants, dirs),
				!c.Bool(localFlag.Name), registryDigestResolver, opts...,
			)
		}
	}
)

func imagesPublishCommand(cfg *Config) *cli.Command {
	return &cli.Command{
		Name:   "publish",
		Usage:  "Publish container images to their registries",
		Flags:  imagesPublishFlags,
		Action: imagesPublishAction(cfg),
	}
}

// releaseImageList runs make in every release directory, so a test replaces it.
var releaseImageList = images.BuildImages

var imagesCheckOperatorAction = func(cfg *Config) func(ctx context.Context, c *cli.Command) error {
	return func(_ context.Context, _ *cli.Command) error {
		configureLogging("images-check-operator.log")

		// The operator publishes to registries of its own, so it is named apart from the
		// release directories rather than discovered with them.
		dirs := append(images.ReleaseDirs(), operator.DirName)
		built, err := releaseImageList(cfg.RepoRootDir, dirs...)
		if err != nil {
			return err
		}
		if err := operatorimages.Check(built); err != nil {
			return err
		}
		logrus.WithField("images", len(built)).Info("The operator deploys every image built here")
		return nil
	}
}

func imagesCheckOperatorCommand(cfg *Config) *cli.Command {
	return &cli.Command{
		Name:   "check-operator",
		Usage:  "Check that the operator deploys every image this repo builds",
		Action: imagesCheckOperatorAction(cfg),
	}
}

// scanRequest builds the scan request. Release decides which bucket the
// scanner files results under, so a hashrelease is not a release.
func scanRequest(c *cli.Command, cfg *Config, dirs []string, stream, productCode string) (*images.ScanRequest, error) {
	if !c.Bool(imageScanFlag.Name) {
		return nil, nil
	}
	imgs, err := releaseImageList(cfg.RepoRootDir, dirs...)
	if err != nil {
		return nil, err
	}
	return &images.ScanRequest{
		Config:      *imageScanningAPIConfig(c),
		ProductCode: productCode,
		Images:      imgs,
		Stream:      stream,
		Release:     !c.Bool(hashreleaseFlag.Name),
		OutputDir:   cfg.TmpDir,
		DryRun:      c.Bool(localFlag.Name),
	}, nil
}
