package repo

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/samber/lo"
	"golang.org/x/xerrors"
	"gopkg.in/yaml.v3"

	"github.com/aquasecurity/trivy/pkg/log"
	"github.com/aquasecurity/trivy/pkg/utils/fsutils"
)

const (
	defaultVEXHubURL = "https://github.com/aquasecurity/vexhub"
	vexDir           = "vex"
	repoDir          = "repositories"
)

type ManagerOption func(indexer *Manager)

func WithWriter(w io.Writer) ManagerOption {
	return func(manager *Manager) {
		manager.w = w
	}
}

type Config struct {
	Repositories []Repository `json:"repositories"`
}

func (c *Config) EnabledRepositories() []Repository {
	return lo.Filter(c.Repositories, func(r Repository, _ int) bool {
		return r.Enabled
	})
}

type Options struct {
	Insecure bool
}

// Manager manages the repositories
type Manager struct {
	w          io.Writer
	configFile string
	cacheDir   string
}

func NewManager(cacheRoot string, opts ...ManagerOption) *Manager {
	m := &Manager{
		w:          os.Stdout,
		configFile: filepath.Join(fsutils.TrivyHomeDir(), vexDir, "repository.yaml"),
		cacheDir:   filepath.Join(cacheRoot, vexDir),
	}
	for _, opt := range opts {
		opt(m)
	}

	return m
}

func (m *Manager) writeConfig(conf Config) error {
	if err := os.MkdirAll(filepath.Dir(m.configFile), 0o700); err != nil {
		return xerrors.Errorf("failed to mkdir: %w", err)
	}
	f, err := os.Create(m.configFile)
	if err != nil {
		return xerrors.Errorf("failed to create a file: %w", err)
	}
	defer f.Close()

	e := yaml.NewEncoder(f)
	e.SetIndent(2)
	if err = e.Encode(conf); err != nil {
		return xerrors.Errorf("JSON encode error: %w", err)
	}

	return nil
}

func (m *Manager) Config(ctx context.Context) (Config, error) {
	if !fsutils.FileExists(m.configFile) {
		log.DebugContext(ctx, "No repository config found", log.String("path", m.configFile))
		if err := m.Init(ctx); err != nil {
			return Config{}, xerrors.Errorf("unable to initialize the VEX repository config: %w", err)
		}
	}

	f, err := os.Open(m.configFile)
	if err != nil {
		return Config{}, xerrors.Errorf("unable to open a file: %w", err)
	}
	defer f.Close()

	var conf Config
	if err = yaml.NewDecoder(f).Decode(&conf); err != nil {
		return conf, xerrors.Errorf("unable to decode metadata: %w", err)
	}

	for i, repo := range conf.Repositories {
		conf.Repositories[i].dir = filepath.Join(m.cacheDir, repoDir, repo.Name)
	}

	return conf, nil
}

func (m *Manager) Init(ctx context.Context) error {
	if fsutils.FileExists(m.configFile) {
		log.InfoContext(ctx, "The configuration file already exists", log.String("path", m.configFile))
		return nil
	}

	err := m.writeConfig(Config{
		Repositories: []Repository{
			{
				Name:    "default",
				URL:     defaultVEXHubURL,
				Enabled: true,
			},
		},
	})
	if err != nil {
		return xerrors.Errorf("failed to write the default config: %w", err)
	}
	log.InfoContext(ctx, "The default repository config has been created", log.FilePath(m.configFile))
	return nil
}

func (m *Manager) DownloadRepositories(ctx context.Context, names []string, opts Options) error {
	conf, err := m.Config(ctx)
	if err != nil {
		return xerrors.Errorf("unable to read config: %w", err)
	}

	repos := lo.Filter(conf.EnabledRepositories(), func(r Repository, _ int) bool {
		return len(names) == 0 || slices.Contains(names, r.Name)
	})
	if len(repos) == 0 {
		log.WarnContext(ctx, "No enabled repositories found in config", log.String("path", m.configFile))
		return nil
	}

	for _, repo := range repos {
		if err = repo.Update(ctx, opts); err != nil {
			return xerrors.Errorf("failed to update the repository: %w", err)
		}
	}
	return nil
}

// List returns a list of all repositories in the configuration
func (m *Manager) List(ctx context.Context) error {
	conf, err := m.Config(ctx)
	if err != nil {
		return xerrors.Errorf("unable to read config: %w", err)
	}

	var output strings.Builder

	output.WriteString(fmt.Sprintf("VEX Repositories (config: %s)\n\n", m.configFile))

	if len(conf.Repositories) == 0 {
		output.WriteString("No repositories configured.\n")
	} else {
		for _, repo := range conf.Repositories {
			status := "Enabled"
			if !repo.Enabled {
				status = "Disabled"
			}
			output.WriteString(fmt.Sprintf("- Name: %s\n  URL: %s\n  Status: %s\n\n", repo.Name, repo.URL, status))
		}
	}

	if _, err = io.WriteString(m.w, output.String()); err != nil {
		return xerrors.Errorf("failed to write output: %w", err)
	}

	return nil
}

func (m *Manager) Clear() error {
	return os.RemoveAll(m.cacheDir)
}
