// Copyright (c) 2026 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.

package imagefactory

import (
	"context"
	"fmt"
	"net/url"
	"time"

	"github.com/cosi-project/runtime/pkg/safe"
	"github.com/cosi-project/runtime/pkg/state"
	"github.com/siderolabs/image-factory/pkg/client"
	"github.com/siderolabs/image-factory/pkg/schematic"

	"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
	"github.com/siderolabs/omni/internal/pkg/auth/actor"
)

// Client is the image factory client.
type Client struct {
	*client.Client

	state state.State
	host  string
}

// NewClient creates a new image factory client.
func NewClient(omniState state.State, imageFactoryBaseURL string) (*Client, error) {
	factoryClient, err := client.New(imageFactoryBaseURL)
	if err != nil {
		return nil, err
	}

	baseURL, err := url.Parse(imageFactoryBaseURL)
	if err != nil {
		return nil, fmt.Errorf("failed to parse image factory base URL %q: %w", imageFactoryBaseURL, err)
	}

	return &Client{
		state:  omniState,
		Client: factoryClient,
		host:   baseURL.Host,
	}, nil
}

// Host returns the host of the image factory client.
func (cli *Client) Host() string {
	if cli == nil {
		return ""
	}

	return cli.host
}

// EnsuredSchematic contains information on the ensured schematics.
type EnsuredSchematic struct {
	// FullID is the ID of the full schematic - with the whole content of the schematic.Schematic, i.e., the extra kernel arguments, META values, and system extensions.
	FullID string

	// PlainID is the ID of the plain schematic - with only the system extensions.
	PlainID string
}

// EnsureSchematic ensures that the given schematic exists in the image factory.
//
// It ensures two schematics: one with the full content of the schematic.Schematic, and another with only the system extensions.
//
// We do not call the image factory for the schematics that are already known to (cached by) Omni.
func (cli *Client) EnsureSchematic(ctx context.Context, inputSchematic schematic.Schematic) (EnsuredSchematic, error) {
	fullSchematicID, err := cli.ensureSingleSchematic(ctx, inputSchematic)
	if err != nil {
		return EnsuredSchematic{}, fmt.Errorf("failed to ensure single schematic: %w", err)
	}

	plainSchematic := schematic.Schematic{
		Customization: schematic.Customization{
			SystemExtensions: schematic.SystemExtensions{
				OfficialExtensions: inputSchematic.Customization.SystemExtensions.OfficialExtensions,
			},
		},
	}

	plainSchematicID, err := plainSchematic.ID()
	if err != nil {
		return EnsuredSchematic{}, fmt.Errorf("failed to generate plain schematic ID: %w", err)
	}

	if plainSchematicID != fullSchematicID {
		plainSchematicID, err = cli.ensureSingleSchematic(ctx, plainSchematic)
		if err != nil {
			return EnsuredSchematic{}, fmt.Errorf("failed to ensure plain schematic: %w", err)
		}
	}

	return EnsuredSchematic{
		FullID:  fullSchematicID,
		PlainID: plainSchematicID,
	}, nil
}

func (cli *Client) ensureSingleSchematic(ctx context.Context, schematic schematic.Schematic) (string, error) {
	schematicID, err := schematic.ID()
	if err != nil {
		return "", fmt.Errorf("failed to generate schematic ID: %w", err)
	}

	schematicResource := omni.NewSchematic(
		schematicID,
	)

	res, err := safe.StateGetByID[*omni.Schematic](ctx, cli.state, schematicID)
	if err != nil && !state.IsNotFoundError(err) {
		return "", err
	}

	if res != nil {
		return schematicID, nil
	}

	callCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	if _, err = cli.SchematicCreate(callCtx, schematic); err != nil {
		return "", fmt.Errorf("failed to create schematic: %w", err)
	}

	ctx = actor.MarkContextAsInternalActor(ctx)

	if err = cli.state.Create(ctx, schematicResource); err != nil && !state.IsConflictError(err) {
		return "", err
	}

	return schematicID, nil
}
