package server

import (
	"context"
	"encoding/json"
	"fmt"
	"path/filepath"
	"sync/atomic"

	"github.com/pkg/errors"

	"github.com/kopia/kopia/fs"
	"github.com/kopia/kopia/fs/localfs"
	"github.com/kopia/kopia/internal/ospath"
	"github.com/kopia/kopia/internal/serverapi"
	"github.com/kopia/kopia/internal/uitask"
	"github.com/kopia/kopia/internal/units"
	"github.com/kopia/kopia/snapshot"
	"github.com/kopia/kopia/snapshot/policy"
	"github.com/kopia/kopia/snapshot/upload"
)

type estimateTaskProgress struct {
	ctrl uitask.Controller
}

func (p estimateTaskProgress) Processing(_ context.Context, dirname string) {
	p.ctrl.ReportProgressInfo(dirname)
}

func (p estimateTaskProgress) Error(ctx context.Context, dirname string, err error, isIgnored bool) {
	if isIgnored {
		userLog(ctx).Errorf("ignored error in %v: %v", dirname, err)
	} else {
		userLog(ctx).Errorf("error in %v: %v", dirname, err)
	}
}

func (p estimateTaskProgress) Stats(ctx context.Context, st *snapshot.Stats, included, excluded upload.SampleBuckets, excludedDirs []string, final bool) {
	_ = excludedDirs
	_ = final

	p.ctrl.ReportCounters(map[string]uitask.CounterValue{
		"Bytes":                uitask.BytesCounter(atomic.LoadInt64(&st.TotalFileSize)),
		"Excluded Bytes":       uitask.BytesCounter(atomic.LoadInt64(&st.ExcludedTotalFileSize)),
		"Files":                uitask.SimpleCounter(int64(atomic.LoadInt32(&st.TotalFileCount))),
		"Directories":          uitask.SimpleCounter(int64(atomic.LoadInt32(&st.TotalDirectoryCount))),
		"Excluded Files":       uitask.SimpleCounter(int64(atomic.LoadInt32(&st.ExcludedFileCount))),
		"Excluded Directories": uitask.SimpleCounter(int64(atomic.LoadInt32(&st.ExcludedDirCount))),
		"Errors":               uitask.ErrorCounter(int64(atomic.LoadInt32(&st.ErrorCount))),
		"Ignored Errors":       uitask.ErrorCounter(int64(atomic.LoadInt32(&st.IgnoredErrorCount))),
	})

	if final {
		logBucketSamples(ctx, included, "Included", false)
		logBucketSamples(ctx, excluded, "Excluded", true)
	}
}

func logBucketSamples(ctx context.Context, buckets upload.SampleBuckets, prefix string, showExamples bool) {
	hasAny := false

	for i, bucket := range buckets {
		if bucket.Count == 0 {
			continue
		}

		var sizeRange string

		if i == 0 {
			sizeRange = fmt.Sprintf("< %-6v",
				units.BytesString(bucket.MinSize))
		} else {
			sizeRange = fmt.Sprintf("%-6v...%6v",
				units.BytesString(bucket.MinSize),
				units.BytesString(buckets[i-1].MinSize))
		}

		userLog(ctx).Infof("%v files %v: %7v files, total size %v\n",
			prefix,
			sizeRange,
			bucket.Count, units.BytesString(bucket.TotalSize))

		hasAny = true

		if showExamples && len(bucket.Examples) > 0 {
			userLog(ctx).Info("Examples:")

			for _, sample := range bucket.Examples {
				userLog(ctx).Infof(" - %v\n", sample)
			}
		}
	}

	if !hasAny {
		userLog(ctx).Infof("%v files: None", prefix)
	}
}

var _ upload.EstimateProgress = estimateTaskProgress{}

func handleEstimate(ctx context.Context, rc requestContext) (any, *apiError) {
	var req serverapi.EstimateRequest

	if err := json.Unmarshal(rc.body, &req); err != nil {
		return nil, requestError(serverapi.ErrorMalformedRequest, "malformed request body")
	}

	resolvedRoot := filepath.Clean(ospath.ResolveUserFriendlyPath(req.Root, true))

	e, err := localfs.NewEntry(resolvedRoot)
	if err != nil {
		return nil, internalServerError(errors.Wrap(err, "can't get local fs entry"))
	}

	dir, ok := e.(fs.Directory)
	if !ok {
		return nil, internalServerError(errors.New("estimation is only supported on directories"))
	}

	taskIDChan := make(chan string)

	policyTree, err := policy.TreeForSourceWithOverride(ctx, rc.rep, snapshot.SourceInfo{
		Host:     rc.rep.ClientOptions().Hostname,
		UserName: rc.rep.ClientOptions().Username,
		Path:     resolvedRoot,
	}, req.PolicyOverride)
	if err != nil {
		return nil, internalServerError(errors.Wrap(err, "unable to get policy tree"))
	}

	// launch a goroutine that will continue the estimate and can be observed in the Tasks UI.

	//nolint:errcheck
	go rc.srv.taskManager().Run(ctx, "Estimate", resolvedRoot, func(ctx context.Context, ctrl uitask.Controller) error {
		taskIDChan <- ctrl.CurrentTaskID()

		estimatectx, cancel := context.WithCancel(ctx)
		defer cancel()

		ctrl.OnCancel(cancel)

		return upload.Estimate(estimatectx, dir, policyTree, estimateTaskProgress{ctrl}, req.MaxExamplesPerBucket)
	})

	taskID := <-taskIDChan

	task, ok := rc.srv.taskManager().GetTask(taskID)
	if !ok {
		return nil, internalServerError(errors.New("task not found"))
	}

	return task, nil
}
