package devops

import (
	"context"
	"fmt"
	"os"
	"slices"
	"sort"

	"gopkg.in/yaml.v3"
)

// Catalog is the set of operation definitions the server is running with.
type Catalog struct {
	byName map[string]*Operation
	names  []string
}

// NewCatalog builds a catalog from operation definitions.
func NewCatalog(ops []*Operation) *Catalog {
	c := &Catalog{byName: make(map[string]*Operation, len(ops))}
	for _, op := range ops {
		if op == nil || op.Name == "" {
			continue
		}
		c.byName[op.Name] = op
	}
	c.names = make([]string, 0, len(c.byName))
	for name := range c.byName {
		c.names = append(c.names, name)
	}
	sort.Strings(c.names)
	return c
}

// LoadCatalog reads the catalog from the DevOps server, which is the source of
// truth for what the server will actually accept.
func LoadCatalog(ctx context.Context, cli *Client) (*Catalog, error) {
	ops, err := cli.ListOperations(ctx, ListOperationsParams{})
	if err != nil {
		return nil, fmt.Errorf("list operations: %w", err)
	}
	return NewCatalog(ops), nil
}

// LoadCatalogFromFile reads the catalog from an `operations.yaml` file, for
// offline use. It can drift from the deployed server, so the API is preferred.
func LoadCatalogFromFile(path string) (*Catalog, error) {
	raw, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	var ops []*Operation
	if err := yaml.Unmarshal(raw, &ops); err != nil {
		return nil, fmt.Errorf("parse %s: %w", path, err)
	}
	if len(ops) == 0 {
		return nil, fmt.Errorf("%s contains no operation definitions", path)
	}
	return NewCatalog(ops), nil
}

// Get returns an operation definition by name.
func (c *Catalog) Get(name string) (*Operation, bool) {
	op, ok := c.byName[name]
	return op, ok
}

// Names returns every operation name, sorted.
func (c *Catalog) Names() []string {
	return slices.Clone(c.names)
}

// CatalogFilter narrows a catalog listing.
type CatalogFilter struct {
	Category        string
	Target          string
	IncludeInternal bool
}

// List returns the operations matching the filter, ordered by name.
func (c *Catalog) List(f CatalogFilter) []*Operation {
	out := make([]*Operation, 0, len(c.names))
	for _, name := range c.names {
		op := c.byName[name]
		if op.Internal && !f.IncludeInternal {
			continue
		}
		if f.Target != "" && string(op.Target) != f.Target {
			continue
		}
		if f.Category != "" && !slices.Contains(op.Categories, f.Category) {
			continue
		}
		out = append(out, op)
	}
	return out
}
