package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/wagoodman/go-partybus"

	"github.com/anchore/grype/grype/db"
	"github.com/anchore/grype/grype/differ"
	"github.com/anchore/grype/grype/event"
	"github.com/anchore/grype/internal/bus"
	"github.com/anchore/grype/internal/log"
	"github.com/anchore/grype/internal/ui"
	"github.com/anchore/stereoscope"
)

var dbDiffOutputFormat string

const deleteFlag string = "delete"

var dbDiffCmd = &cobra.Command{
	Use:   "diff [flags] base_db_url target_db_url",
	Short: "diff two DBs and display the result",
	Args:  cobra.MaximumNArgs(2),
	RunE:  runDBDiffCmd,
}

func init() {
	dbDiffCmd.Flags().StringVarP(&dbDiffOutputFormat, "output", "o", "table", "format to display results (available=[table, json])")
	dbDiffCmd.Flags().BoolP(deleteFlag, "d", false, "delete downloaded databases after diff occurs")

	dbCmd.AddCommand(dbDiffCmd)
}

func startDBDiffCmd(base string, target string, deleteDatabases bool) <-chan error {
	errs := make(chan error)
	go func() {
		defer close(errs)
		d, err := differ.NewDiffer(appConfig.DB.ToCuratorConfig())
		if err != nil {
			errs <- err
			return
		}

		if err := d.SetBaseDB(base); err != nil {
			errs <- err
			return
		}

		if err := d.SetTargetDB(target); err != nil {
			errs <- err
			return
		}

		diff, err := d.DiffDatabases()
		if err != nil {
			errs <- err
			return
		}

		if len(*diff) == 0 {
			fmt.Println("Databases are identical!")
		} else {
			err := d.Present(dbDiffOutputFormat, diff, os.Stdout)
			if err != nil {
				errs <- err
			}
		}

		if deleteDatabases {
			errs <- d.DeleteDatabases()
		}

		bus.Publish(partybus.Event{
			Type:  event.NonRootCommandFinished,
			Value: "",
		})
	}()
	return errs
}

func runDBDiffCmd(cmd *cobra.Command, args []string) error {
	reporter, closer, err := reportWriter()
	defer func() {
		if err := closer(); err != nil {
			log.Warnf("unable to write to report destination: %+v", err)
		}
	}()
	if err != nil {
		return err
	}

	deleteDatabases, err := cmd.Flags().GetBool(deleteFlag)
	if err != nil {
		return err
	}

	var base, target string

	switch len(args) {
	case 0:
		log.Info("base_db_url and target_db_url not provided; fetching most recent")
		base, target, err = getDefaultURLs()
		if err != nil {
			return err
		}
	case 1:
		log.Info("target_db_url not provided; fetching most recent")
		base = args[0]
		_, target, err = getDefaultURLs()
		if err != nil {
			return err
		}
	default:
		base = args[0]
		target = args[1]
	}

	return eventLoop(
		startDBDiffCmd(base, target, deleteDatabases),
		setupSignals(),
		eventSubscription,
		stereoscope.Cleanup,
		ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
	)
}

func getDefaultURLs() (baseURL string, targetURL string, err error) {
	dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig())
	if err != nil {
		return "", "", err
	}

	listing, err := dbCurator.ListingFromURL()
	if err != nil {
		return "", "", err
	}

	supportedSchema := dbCurator.SupportedSchema()
	available, exists := listing.Available[supportedSchema]
	if len(available) < 2 || !exists {
		return "", "", stderrPrintLnf("Not enough databases available for the current schema to diff (%d)", supportedSchema)
	}

	targetURL = available[0].URL.String()
	baseURL = available[1].URL.String()

	return baseURL, targetURL, nil
}
