package export

import (
	"context"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/spf13/cobra"

	"github.com/ankitpokhrel/shopctl/internal/api"
	"github.com/ankitpokhrel/shopctl/internal/cmdutil"
	"github.com/ankitpokhrel/shopctl/internal/config"
	"github.com/ankitpokhrel/shopctl/internal/engine"
	"github.com/ankitpokhrel/shopctl/internal/runner"
	"github.com/ankitpokhrel/shopctl/internal/runner/backup/customer"
	"github.com/ankitpokhrel/shopctl/internal/runner/backup/product"
	"github.com/ankitpokhrel/shopctl/pkg/tlog"
)

const (
	helpText = `Export exports Shopify resources based on the given filters.`
	examples = `$ shopctl export --resource product --output-dir /path/to/dir --name product_export

# Export products and customers from another store
$ shopctl export -c store2 -r product -r customer -o /path/to/dir

# Export premium on-sale products and customers created starting 2025
$ shopctl export -r product="tag:on-sale AND tag:premium" -r customer=created_at:>=2025-01-01 -o /path/to/dir

# You can use 'list' command to prepare filters
$ shopctl export -c mycontext -r product="$(shopctl product list --tags on-sale --type Bags --print-query)" -o /path/to/dir

# Dry run executes the export without creating final files. This will still create files in temporary location.
# Use this option if you want to verify your export without the risk of saving data to the unintented location.
$ shopctl export -r product="tag:on-sale" --dry-run
`
)

var verbosity int

type flag struct {
	outDir    string
	name      string
	resources []config.BackupResource
	dryRun    bool
	quiet     bool
}

func (f *flag) parse(cmd *cobra.Command) {
	dir, err := cmd.Flags().GetString("output-dir")
	cmdutil.ExitOnErr(err)

	name, err := cmd.Flags().GetString("name")
	cmdutil.ExitOnErr(err)

	resources, err := cmd.Flags().GetStringArray("resource")
	cmdutil.ExitOnErr(err)

	if len(resources) == 0 || dir == "" {
		cmdutil.ExitOnErr(cmdutil.HelpErrorf("Error: resources to export and output directory is required", examples))
	}

	dryRun, err := cmd.Flags().GetBool("dry-run")
	cmdutil.ExitOnErr(err)

	quiet, err := cmd.Flags().GetBool("quiet")
	cmdutil.ExitOnErr(err)

	f.outDir = dir
	f.name = name
	f.resources = cmdutil.ParseBackupResource(resources)
	f.dryRun = dryRun
	f.quiet = quiet
}

// NewCmdExport creates a new export command.
func NewCmdExport() *cobra.Command {
	cmd := cobra.Command{
		Use:         "export",
		Short:       "Export Shopify resources",
		Long:        helpText,
		Example:     examples,
		Annotations: map[string]string{"cmd:main": "true"},
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			cmdutil.ExitOnErr(preRun(cmd, args))
			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			ctx := cmd.Context().Value(cmdutil.KeyContext).(*config.StoreContext)
			client := cmd.Context().Value(cmdutil.KeyGQLClient).(*api.GQLClient)
			logger := cmd.Context().Value(cmdutil.KeyLogger).(*tlog.Logger)

			cmdutil.ExitOnErr(run(cmd, client, ctx, logger))
			return nil
		},
	}

	cmd.Flags().StringP("output-dir", "o", "", "Root output directory to save files to")
	cmd.Flags().StringP("name", "n", "", "Name of the generated export folder (default autogenerated)")
	cmd.Flags().StringArrayP("resource", "r", []string{}, "Resources to export (accepts filters)")
	cmd.Flags().Bool("dry-run", false, "Print logs without creating an actual backup file")
	cmd.Flags().Bool("quiet", false, "Do not print anything to stdout")
	cmd.Flags().CountVarP(&verbosity, "verbose", "v", "Set the verbosity level (e.g., -v, -vv, -vvv)")

	cmd.Flags().SortFlags = false

	return &cmd
}

func preRun(cmd *cobra.Command, _ []string) error {
	cfg, err := config.NewShopConfig()
	if err != nil {
		return err
	}

	ctx, err := cmdutil.GetContext(cmd, cfg)
	if err != nil {
		return err
	}

	v, err := cmd.Flags().GetCount("verbose")
	if err != nil {
		return err
	}

	quiet, err := cmd.Flags().GetBool("quiet")
	if err != nil {
		return err
	}
	lgr := tlog.New(tlog.VerboseLevel(v), quiet)

	gqlClient := api.NewGQLClient(ctx)
	cmd.SetContext(context.WithValue(cmd.Context(), cmdutil.KeyContext, ctx))
	cmd.SetContext(context.WithValue(cmd.Context(), cmdutil.KeyGQLClient, gqlClient))
	cmd.SetContext(context.WithValue(cmd.Context(), cmdutil.KeyLogger, lgr))

	return nil
}

//nolint:gocyclo
func run(cmd *cobra.Command, client *api.GQLClient, ctx *config.StoreContext, logger *tlog.Logger) error {
	flag := &flag{}
	flag.parse(cmd)

	bkpEng := engine.NewBackup(
		ctx.Store,
		engine.WithBackupDir(flag.name),
	)
	eng := engine.New(bkpEng)

	var (
		wg      sync.WaitGroup
		rnr     runner.Runner
		start   time.Time
		counter int

		runners = make([]runner.Runner, 0, len(flag.resources))
	)

	defer func() {
		if !flag.dryRun && counter > 0 {
			err := archive(bkpEng.Root(), flag.outDir, bkpEng.Dir())
			if err != nil {
				logger.Errorf("Error: unable to archive: %s", err.Error())
			}
		}
		logger.Infof("Export complete in %s", time.Since(start))

		if !flag.quiet && counter > 0 {
			summarize(flag, bkpEng, runners)
		}
	}()

	for _, resource := range flag.resources {
		switch engine.ResourceType(resource.Resource) {
		case engine.Product:
			rnr = product.NewRunner(eng, client, resource.Query, logger)
		case engine.Customer:
			rnr = customer.NewRunner(eng, client, logger)
		default:
			logger.Warnf("Skipping '%s': invalid resource", resource)
			continue
		}
		runners = append(runners, rnr)
	}

	if flag.dryRun {
		logger.Warn("This is a dry run. API calls will be made but final backup files won't be created.")
	}
	logger.V(tlog.VL1).Infof("Using context %q", ctx.Alias)
	logger.V(tlog.VL1).Infof("Using store %q", ctx.Store)

	start = time.Now()
	for _, rnr := range runners {
		wg.Add(1)

		go func(r runner.Runner) {
			defer wg.Done()

			if err := r.Run(); err != nil {
				logger.Errorf("%s runner exited with err: %s", r.Kind(), err.Error())
			}
		}(rnr)
	}
	wg.Wait()

	for _, rnr := range runners {
		stats := rnr.Stats()
		counter += stats[rnr.Kind()].Count
	}

	if flag.quiet {
		return nil
	}
	if counter == 0 {
		logger.Info("No matching records found for the given criteria")
	}
	return nil
}

func archive(from string, to string, name string) error {
	const modDir = 0o755
	if err := os.MkdirAll(to, modDir); err != nil {
		return err
	}
	return cmdutil.Archive(from, to, name)
}

func summarize(f *flag, bkpEng *engine.Backup, runners []runner.Runner) {
	fmt.Println()
	cmdutil.SummaryTitle("EXPORT SUMMARY", cmdutil.RepeatedEquals)
	fmt.Printf(`Store: %s
Resources: %s
Path: %s
File: %s.tar.gz
`,
		bkpEng.Store(),
		func() string {
			resources := make([]string, 0, len(f.resources))
			for _, r := range f.resources {
				resources = append(resources, r.Resource)
			}
			return strings.Join(resources, ",")
		}(),
		f.outDir, bkpEng.Dir(),
	)
	for _, rnr := range runners {
		fmt.Println()
		stats := rnr.Stats()
		for _, rt := range engine.GetPrimaryResourceTypes() {
			st, ok := stats[rt]
			if !ok {
				continue
			}
			cmdutil.SummaryTitle(strings.ToTitle(string(rt)), cmdutil.RepeatedDashes)
			fmt.Println(st.String())
			fmt.Println()
		}
	}
}
