package cmd

import (
	"fmt"
	"log"
	"slices"
	"time"

	"github.com/Jaculabilis/intake/core"
	"github.com/spf13/cobra"
)

var actionExecuteCmd = &cobra.Command{
	Use:     "execute",
	Aliases: []string{"exec"},
	Short:   "Run a source action for an item",
	Long: fmt.Sprintf(`Execute a source action for an item.

The item must declare support for the action by having the action's name
in its "action" field. Use --force to execute the action anyway.

The "fetch" action is special and does not execute for any specific item.
Use "intake source fetch" to run the fetch action.

In a dry run, the item will be printed in the chosen format and not updated.

%s`, makeFormatHelpText()),
	Run: func(cmd *cobra.Command, args []string) {
		actionExecute(
			stringArg(cmd, "source"),
			stringArg(cmd, "action"),
			stringArg(cmd, "item"),
			stringArg(cmd, "format"),
			stringArg(cmd, "timeout"),
			boolArg(cmd, "dry-run"),
			boolArg(cmd, "diff"),
			boolArg(cmd, "force"),
		)
	},
}

func init() {
	actionCmd.AddCommand(actionExecuteCmd)

	actionExecuteCmd.PersistentFlags().StringP("source", "s", "", "Source of the item")
	actionExecuteCmd.MarkFlagRequired("source")

	actionExecuteCmd.PersistentFlags().StringP("item", "i", "", "Item to run action on")
	actionExecuteCmd.MarkFlagRequired("item")

	actionExecuteCmd.PersistentFlags().StringP("action", "a", "", "Action to run")
	actionExecuteCmd.MarkFlagRequired("action")

	actionExecuteCmd.Flags().StringP("format", "f", "headlines", "Feed format for returned items")

	actionExecuteCmd.Flags().StringP("timeout", "t", core.DefaultTimeout.String(),
		fmt.Sprintf("Timeout duration (default: %s)", core.DefaultTimeout.String()))

	actionExecuteCmd.Flags().Bool("dry-run", false, "Instead of updating the item, print it")

	actionExecuteCmd.Flags().Bool("diff", false, "Show which fields of the item changed")

	actionExecuteCmd.Flags().Bool("force", false, "Execute the action even if the item does not support it")
}

func actionExecute(
	source string,
	action string,
	itemId string,
	format string,
	timeout string,
	dryRun bool,
	diff bool,
	force bool,
) {
	formatter := formatAs(format)

	if source == "" {
		log.Fatal("error: --source is empty")
	}
	if action == "" {
		log.Fatal("error: --action is empty")
	}
	if itemId == "" {
		log.Fatal("error: --item is empty")
	}
	duration, err := time.ParseDuration(timeout)
	if err != nil {
		log.Fatalf("error: invalid duration: %v", err)
	}

	db := openAndMigrateDb()

	state, envs, argv, err := core.GetSourceActionInputs(db, source, action)
	if err != nil {
		log.Fatalf("error: failed to load data for %s: %v", source, err)
	}

	item, err := core.GetItem(db, source, itemId)
	if err != nil {
		log.Fatalf("error: failed to get item: %v", err)
	}

	if item.Action[action] == nil {
		if force {
			log.Printf("warning: force-executing %s on %s/%s", action, source, itemId)
		} else {
			log.Fatalf("error: %s/%s does not support %s", source, itemId, action)
		}
	}

	newItem, newState, errItem, err := core.ExecuteItemAction(item, argv, envs, state, duration)
	if err != nil {
		core.AddErrorItem(db, errItem)
		log.Fatalf("error executing %s: %v", action, err)
	}

	if diff {
		if item.Title != newItem.Title {
			log.Printf("title: %s => %s", item.Title, newItem.Title)
		}
		if item.Author != newItem.Author {
			log.Printf("author: %s => %s", item.Author, newItem.Author)
		}
		if item.Body != newItem.Body {
			log.Printf("body: %s => %s", item.Body, newItem.Body)
		}
		if item.Link != newItem.Link {
			log.Printf("link: %s => %s", item.Link, newItem.Link)
		}
		if item.Time != newItem.Time {
			log.Printf("time: %d => %d", item.Time, newItem.Time)
		}
		if core.ItemsAreEqual(item, newItem) {
			log.Printf("no changes\n")
		}
		if !slices.Equal(state, newState) {
			log.Printf("state changed (%d => %d bytes)", len(state), len(newState))
		}
	}

	if dryRun {
		fmt.Println(formatter(newItem))
		return
	}

	if err = db.Transact(func(tx core.DB) error {
		if _err := core.UpdateItems(tx, []core.Item{newItem}); err != nil {
			return fmt.Errorf("failed to update item: %v", _err)
		}
		if _err := core.SetState(tx, source, newState); err != nil {
			return fmt.Errorf("failed to set state for %s: %v", source, _err)
		}
		return nil
	}); err != nil {
		log.Fatalf("error: %v", err)
	}
}