package reporter

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"time"

	"github.com/cloudflare/pint/internal/git"
	"github.com/cloudflare/pint/internal/output"
)

const (
	BitBucketDescription = "pint is a Prometheus rule linter/validator.\n" +
		"It will inspect all Prometheus recording and alerting rules for problems that could prevent these from working correctly.\n" +
		"Checks can be either offline (static checks using only rule definition) or online (validate rule against live Prometheus server)."
)

func NewBitBucketReporter(version, uri string, timeout time.Duration, token, project, repo string, maxComments int, showDuplicates bool, gitCmd git.CommandRunner) BitBucketReporter {
	slog.LogAttrs(
		context.Background(), slog.LevelInfo,
		"Will report problems to BitBucket",
		slog.String("uri", uri),
		slog.String("timeout", output.HumanizeDuration(timeout)),
		slog.String("project", project),
		slog.String("repo", repo),
		slog.Int("maxComments", maxComments),
	)
	return BitBucketReporter{
		api:    newBitBucketAPI(version, uri, timeout, token, project, repo, maxComments, showDuplicates),
		gitCmd: gitCmd,
	}
}

// BitBucketReporter send linter results to BitBucket using
// https://docs.atlassian.com/bitbucket-server/rest/7.8.0/bitbucket-code-insights-rest.html
type BitBucketReporter struct {
	api    *bitBucketAPI
	gitCmd git.CommandRunner
}

func (bb BitBucketReporter) Submit(ctx context.Context, summary Summary) (err error) {
	gitInfo, err := git.Describe(bb.gitCmd)
	if err != nil {
		return fmt.Errorf("failed to get git info: %w", err)
	}
	slog.LogAttrs(ctx, slog.LevelInfo, "Got HEAD commit from git", slog.String("commit", gitInfo.HeadCommit))

	if err = bb.api.deleteReport(gitInfo.HeadCommit); err != nil {
		slog.LogAttrs(ctx, slog.LevelError, "Failed to delete old BitBucket report", slog.Any("err", err))
	}

	if err = bb.api.createReport(summary, gitInfo.HeadCommit); err != nil {
		return fmt.Errorf("failed to create BitBucket report: %w", err)
	}

	var pr *bitBucketPR
	if pr, err = bb.api.findPullRequestForBranch(gitInfo.CurrentBranch, gitInfo.HeadCommit); err != nil {
		return fmt.Errorf("failed to get open pull requests from BitBucket: %w", err)
	}

	if pr != nil {
		slog.LogAttrs(
			ctx, slog.LevelInfo,
			"Found open pull request, reporting problems using comments",
			slog.Int("id", pr.ID),
			slog.String("srcBranch", pr.srcBranch),
			slog.String("srcCommit", pr.srcHead),
			slog.String("dstBranch", pr.dstBranch),
			slog.String("dstCommit", pr.dstHead),
		)

		slog.LogAttrs(ctx, slog.LevelInfo, "Getting pull request changes from BitBucket")
		var changes *bitBucketPRChanges
		if changes, err = bb.api.getPullRequestChanges(pr); err != nil {
			return fmt.Errorf("failed to get pull request changes from BitBucket: %w", err)
		}
		slog.LogAttrs(ctx, slog.LevelDebug, "Got modified files from BitBucket", slog.Any("files", changes.pathModifiedLines))

		var existingComments []bitBucketComment
		if existingComments, err = bb.api.getPullRequestComments(pr); err != nil {
			return fmt.Errorf("failed to get pull request comments from BitBucket: %w", err)
		}
		slog.LogAttrs(ctx, slog.LevelInfo, "Got existing pull request comments from BitBucket", slog.Int("count", len(existingComments)))

		pendingComments := bb.api.makeComments(summary, changes)
		slog.LogAttrs(ctx, slog.LevelInfo, "Generated comments to add to BitBucket", slog.Int("count", len(pendingComments)))

		pendingComments = bb.api.limitComments(pendingComments)
		slog.LogAttrs(
			ctx, slog.LevelInfo, "Will add comments to BitBucket",
			slog.Int("count", len(pendingComments)),
			slog.Int("limit", bb.api.maxComments),
		)

		slog.LogAttrs(ctx, slog.LevelInfo, "Deleting stale comments from BitBucket")
		bb.api.pruneComments(pr, existingComments, pendingComments)

		slog.LogAttrs(ctx, slog.LevelInfo, "Adding missing comments to BitBucket")
		if err = bb.api.addComments(pr, existingComments, pendingComments); err != nil {
			return fmt.Errorf("failed to create BitBucket pull request comments: %w", err)
		}

	} else {
		slog.LogAttrs(
			ctx, slog.LevelInfo,
			"No open pull request found, reporting problems using code insight annotations",
			slog.String("branch", gitInfo.CurrentBranch),
			slog.String("commit", gitInfo.HeadCommit),
		)

		if err = bb.api.deleteAnnotations(gitInfo.HeadCommit); err != nil {
			return fmt.Errorf("failed to delete existing BitBucket code insight annotations: %w", err)
		}

		if err = bb.api.createAnnotations(summary, gitInfo.HeadCommit); err != nil {
			return fmt.Errorf("failed to create BitBucket code insight annotations: %w", err)
		}
	}

	if summary.HasFatalProblems() {
		return errors.New("fatal error(s) reported")
	}

	return nil
}

// BitBucket only allows us to report annotations for modified lines.
// If a high severity problem is detected on a non-modified line we move that annotation
// to the first modified line.
// Without this we could have a report that is marked as failed, but with no annotations
// at all, which would make it more difficult to fix.
// If we can't find any modified line to match with our report then we return 0,
// which will create a file level annotation.
func moveReportedLine(report Report) (reported, original int) {
	reported = -1
	original = -1
	// No changes data, nothing to move.
	if report.Changes == nil {
		return report.Problem.Lines.Last, report.Problem.Lines.Last
	}
	for pl := report.Problem.Lines.First; pl <= report.Problem.Lines.Last; pl++ {
		if original < 0 {
			original = pl
		}
		if report.Changes.Lines.HasAfter(pl) {
			original = pl
			reported = pl
		}
	}

	if reported < 0 {
		if bestLine := report.Changes.Lines.NearestAfter(report.Problem.Lines.First); bestLine > 0 {
			return bestLine, original
		}
	}

	if reported < 0 {
		reported = 0
	}
	return reported, original
}
