package cli

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/pkg/errors"

	"github.com/kopia/kopia/internal/editor"
	"github.com/kopia/kopia/repo"
	"github.com/kopia/kopia/snapshot/policy"
)

const policyEditHelpText = `
# Editing policy for '%v'

# Make changes to the policy, save your file and exit the editor.
# The output must be valid JSON.

# Lines starting with # are comments and automatically removed.

`

const policyEditRetentionHelpText = `  # Retention for snapshots of this directory. Options include:
  #   "keepLatest": number
  #   "keepDaily": number
  #   "keepHourly": number
  #   "keepWeekly": number
  #   "keepMonthly": number
  #   "keepAnnual": number
`

const policyEditFilesHelpText = `
  # Which files to include in snapshots. Options include:
  #   "ignore": ["*.ext", "*.ext2"]
  #   "ignoreDotFiles": [".gitignore", ".kopiaignore"]
  #   "maxFileSize": number
  #   "noParentDotFiles": true
  #   "noParentIgnore": true
  #   "oneFileSystem": false
`

const policyEditSchedulingHelpText = `
  # Snapshot scheduling options. Options include:
  #   "intervalSeconds": number /* 86400-day, 3600-hour, 60-minute */
  #   "timeOfDay": [{"hour":H,"min":M},{"hour":H,"min":M}]
  #   "manual": false /* Only create snapshots manually if set to true. NOTE: cannot be used with the above two fields */
`

type commandPolicyEdit struct {
	targets []string
	global  bool

	out textOutput
}

func (c *commandPolicyEdit) setup(svc appServices, parent commandParent) {
	cmd := parent.Command("edit", "Set snapshot policy for a single directory, user@host or a global policy.")
	cmd.Arg("target", "Target of a policy ('global','user@host','@host') or a path").StringsVar(&c.targets)
	cmd.Flag("global", "Set global policy").BoolVar(&c.global)
	cmd.Action(svc.repositoryWriterAction(c.run))
	c.out.setup(svc)
}

func (c *commandPolicyEdit) run(ctx context.Context, rep repo.RepositoryWriter) error {
	targets, err := policyTargets(ctx, rep, c.global, c.targets)
	if err != nil {
		return err
	}

	for _, target := range targets {
		original, err := policy.GetDefinedPolicy(ctx, rep, target)
		if errors.Is(err, policy.ErrPolicyNotFound) {
			original = &policy.Policy{}
		}

		log(ctx).Infof("Editing policy for %v using external editor...", target)

		s := policyEditHelpText + prettyJSON(original)
		s = insertHelpText(s, `  "retention": {`, policyEditRetentionHelpText)
		s = insertHelpText(s, `  "files": {`, policyEditFilesHelpText)
		s = insertHelpText(s, `  "scheduling": {`, policyEditSchedulingHelpText)

		var updated *policy.Policy

		if err := editor.EditLoop(ctx, "policy.conf", s, func(edited string) error {
			updated = &policy.Policy{}
			d := json.NewDecoder(bytes.NewBufferString(edited))
			d.DisallowUnknownFields()
			// nolint:wrapcheck
			return d.Decode(updated)
		}); err != nil {
			return errors.Wrap(err, "unable to launch editor")
		}

		if jsonEqual(updated, original) {
			log(ctx).Infof("Policy for %v unchanged", target)
			continue
		}

		log(ctx).Infof("Updated policy for %v\n%v", target, prettyJSON(updated))

		c.out.printStdout("Save updated policy? (y/N) ")

		var shouldSave string

		fmt.Scanf("%v", &shouldSave)

		if strings.HasPrefix(strings.ToLower(shouldSave), "y") {
			if err := policy.SetPolicy(ctx, rep, target, updated); err != nil {
				return errors.Wrapf(err, "can't save policy for %v", target)
			}
		}
	}

	return nil
}

func prettyJSON(v *policy.Policy) string {
	var b bytes.Buffer
	e := json.NewEncoder(&b)
	e.SetIndent("", "  ")
	e.Encode(v) //nolint:errcheck,errchkjson

	return b.String()
}

func jsonEqual(v1, v2 *policy.Policy) bool {
	return prettyJSON(v1) == prettyJSON(v2)
}

func insertHelpText(s, lookFor, help string) string {
	p := strings.Index(s, lookFor)
	if p < 0 {
		return s
	}

	return s[0:p] + help + s[p:]
}
