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

import (
	"context"
	"errors"

	"github.com/sirupsen/logrus"

	"github.com/projectcalico/calico/release/internal/plan"
	"github.com/projectcalico/calico/release/internal/steps"
	"github.com/projectcalico/calico/release/internal/utils"
)

const filePerms = 0o644

func Publish(pipeline []Upload, opts ...Option) error {
	s, err := newSettings(artifactsStep, opts)
	if err != nil {
		return err
	}
	if len(pipeline) == 0 {
		return s.Errorf("no uploads to publish")
	}
	var errs []error
	for _, u := range pipeline {
		if u.Skip {
			continue
		}
		if err := u.validate(); err != nil {
			errs = append(errs, err)
		}
	}
	if err := errors.Join(errs...); err != nil {
		return s.Errorf("%w", err)
	}
	s.pipeline = pipeline

	s.Logger().WithField("uploads", len(s.pipeline)).Info("Publishing artifacts")
	if _, err := s.push(s.pipeline); err != nil {
		return err
	}
	s.Logger().Info("Finished publishing artifacts")
	return nil
}

// Node publishes one upload as a plan step named after it.
// The plan checks every destination before any upload runs.
func Node(u Upload, opts ...Option) plan.Node {
	return plan.Node{
		Name:     u.label(),
		When:     func() bool { return !u.Skip },
		Requires: u.Requires,
		Validate: u.validate,
		Do: func() error {
			s, err := newSettings(artifactsStep, opts)
			if err != nil {
				return err
			}
			return s.publishOne(u)
		},
	}
}

// Ordered, and stops at the first failure as a later upload may depend on an earlier one.
// Returns what published, so a record never names an upload that failed or was never reached.
func (s settings) push(uploads []Upload) ([]Upload, error) {
	var done []Upload
	for _, u := range uploads {
		if err := s.publishOne(u); err != nil {
			return done, err
		}
		done = append(done, u)
	}
	return done, nil
}

func (s settings) publishOne(u Upload) error {
	log := s.Logger().WithFields(logrus.Fields{
		"upload": u.label(), "source": u.Source, "destination": u.dest(),
	})

	if u.Skip {
		log.Info("Skipping upload")
		return nil
	}
	// Checked here rather than up front: an earlier upload in the list may
	// be what creates this source.
	if u.Source != "" {
		exists, err := utils.PathExists(u.Source)
		if err != nil {
			return s.Errorf("reading %s source (%s): %w", u.label(), u.Source, err)
		}
		if !exists {
			return s.Errorf("%s source (%s) does not exist", u.label(), u.Source)
		}
	}

	for attempt := 0; ; attempt++ {
		err := u.Handler.Publish(context.Background(), u.Source)
		if err == nil {
			log.Info("Published")
			return nil
		}
		if attempt < steps.MaxRetries {
			log.WithError(err).WithField("attempt", attempt).Warn("Publish failed, retrying")
			continue
		}
		return s.Errorf("publishing %s to %s: %w", u.label(), u.dest(), err)
	}
}

func newSettings(step string, opts []Option) (settings, error) {
	var s settings
	s.Apply([]steps.Option{steps.WithName(step)})
	for _, opt := range opts {
		if err := opt(&s); err != nil {
			return s, s.Errorf("%w", err)
		}
	}
	return s, nil
}
