package plugin

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"

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

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

const indexURL = "https://aquasecurity.github.io/trivy-plugin-index/v1/index.yaml"

type Index struct {
	Version int `yaml:"version"`
	Plugins []struct {
		Name       string `yaml:"name"`
		Version    string `yaml:"version"`
		Maintainer string `yaml:"maintainer"`
		Summary    string `yaml:"summary"`
		Repository string `yaml:"repository"`
		Output     bool   `yaml:"output"`
	} `yaml:"plugins"`
}

func (m *Manager) Update(ctx context.Context, opts Options) error {
	m.logger.InfoContext(ctx, "Updating the plugin index...", log.String("url", m.indexURL))

	// Download the index file to a temporary directory and copy it to the plugins directory,
	// to avoid removing installed plugins.
	tempDir, err := downloader.DownloadToTempDir(ctx, m.indexURL, downloader.Options{Insecure: opts.Insecure})
	if err != nil {
		return xerrors.Errorf("unable to download the plugin index: %w", err)
	}

	err = os.MkdirAll(filepath.Dir(m.indexPath), fs.ModePerm)
	if err != nil {
		return xerrors.Errorf("failed to create plugin index dir: %w", err)
	}

	_, err = fsutils.CopyFile(filepath.Join(tempDir, "index.yaml"), m.indexPath)
	if err != nil {
		return xerrors.Errorf("unable to copy the plugin index file: %w", err)
	}
	return nil
}

func (m *Manager) Search(ctx context.Context, keyword string) error {
	index, err := m.loadIndex()
	if errors.Is(err, os.ErrNotExist) {
		m.logger.ErrorContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.")
		return xerrors.Errorf("plugin index not found: %w", err)
	} else if err != nil {
		return xerrors.Errorf("unable to load the plugin index: %w", err)
	}

	var buf bytes.Buffer
	fmt.Fprintf(&buf, "%-20s %-60s %-20s %s\n", "NAME", "DESCRIPTION", "MAINTAINER", "OUTPUT")
	for _, p := range index.Plugins {
		if keyword == "" || strings.Contains(p.Name, keyword) || strings.Contains(p.Summary, keyword) {
			s := fmt.Sprintf("%-20s %-60s %-20s %s\n", truncateString(p.Name, 20),
				truncateString(p.Summary, 60), truncateString(p.Maintainer, 20),
				lo.Ternary(p.Output, "  ✓", ""))
			buf.WriteString(s)
		}
	}

	if _, err = fmt.Fprint(m.w, buf.String()); err != nil {
		return err
	}

	return nil
}

// tryIndex returns the repository URL if the plugin name is found in the index.
// Otherwise, it returns the input name.
func (m *Manager) tryIndex(ctx context.Context, name string, opts Options) string {
	// If the index file does not exist, download it first.
	if !fsutils.FileExists(m.indexPath) {
		if err := m.Update(ctx, opts); err != nil {
			m.logger.ErrorContext(ctx, "Failed to update the plugin index", log.Err(err))
			return name
		}
	}

	index, err := m.loadIndex()
	if errors.Is(err, os.ErrNotExist) {
		m.logger.WarnContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.")
		return name
	} else if err != nil {
		m.logger.ErrorContext(ctx, "Unable to load the plugin index", log.Err(err))
		return name
	}

	for _, p := range index.Plugins {
		if p.Name == name {
			return p.Repository
		}
	}
	return name
}

func (m *Manager) loadIndex() (*Index, error) {
	f, err := os.Open(m.indexPath)
	if err != nil {
		return nil, xerrors.Errorf("unable to open the index file: %w", err)
	}
	defer f.Close()

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

	return &index, nil
}

func truncateString(str string, num int) string {
	if len(str) <= num {
		return str
	}
	return str[:num-3] + "..."
}
