package cli

import (
	"context"
	"encoding/json"
	"io"
	"os"
	"slices"

	"github.com/pkg/errors"

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

type commandPolicyImport struct {
	policyTargetFlags
	filePath            string
	allowUnknownFields  bool
	deleteOtherPolicies bool

	svc appServices
}

func (c *commandPolicyImport) setup(svc appServices, parent commandParent) {
	cmd := parent.Command("import", "Imports policies from a specified file, or stdin if no file is specified.")
	cmd.Flag("from-file", "File path to import from").StringVar(&c.filePath)
	cmd.Flag("allow-unknown-fields", "Allow unknown fields in the policy file").BoolVar(&c.allowUnknownFields)
	cmd.Flag("delete-other-policies", "Delete all other policies, keeping only those that got imported").BoolVar(&c.deleteOtherPolicies)

	c.policyTargetFlags.setup(cmd)
	c.svc = svc

	cmd.Action(svc.repositoryWriterAction(c.run))
}

func (c *commandPolicyImport) run(ctx context.Context, rep repo.RepositoryWriter) error {
	var input io.Reader

	var err error

	if c.filePath != "" {
		file, err := os.Open(c.filePath)
		if err != nil {
			return errors.Wrap(err, "unable to read policy file")
		}

		defer file.Close() //nolint:errcheck

		input = file
	} else {
		input = c.svc.stdin()
	}

	policies := make(map[string]*policy.Policy)
	d := json.NewDecoder(input)

	if !c.allowUnknownFields {
		d.DisallowUnknownFields()
	}

	err = d.Decode(&policies)
	if err != nil {
		return errors.Wrap(err, "unable to decode policy file as valid json")
	}

	var targetLimit []snapshot.SourceInfo

	if c.global || len(c.targets) > 0 {
		targetLimit, err = c.policyTargets(ctx, rep)
		if err != nil {
			return err
		}
	}

	shouldImportSource := func(target snapshot.SourceInfo) bool {
		if targetLimit == nil {
			return true
		}

		return slices.Contains(targetLimit, target)
	}

	importedSources := make([]string, 0, len(policies))

	for ts, newPolicy := range policies {
		target, err := snapshot.ParseSourceInfo(ts, rep.ClientOptions().Hostname, rep.ClientOptions().Username)
		if err != nil {
			return errors.Wrapf(err, "unable to parse source info: %q", ts)
		}

		if !shouldImportSource(target) {
			continue
		}
		// used for deleteOtherPolicies
		importedSources = append(importedSources, ts)

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

	if c.deleteOtherPolicies {
		err := deleteOthers(ctx, rep, importedSources)
		if err != nil {
			return err
		}
	}

	return nil
}

func deleteOthers(ctx context.Context, rep repo.RepositoryWriter, importedSources []string) error {
	ps, err := policy.ListPolicies(ctx, rep)
	if err != nil {
		return errors.Wrap(err, "failed to list policies")
	}

	for _, p := range ps {
		if !slices.Contains(importedSources, p.Target().String()) {
			if err := policy.RemovePolicy(ctx, rep, p.Target()); err != nil {
				return errors.Wrapf(err, "can't delete policy for %v", p.Target())
			}
		}
	}

	return nil
}
