/*
 * Copyright 2021-2024 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package platform

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"github.com/JetBrains/qodana-cli/internal/platform/msg"
	"github.com/JetBrains/qodana-cli/internal/platform/qdenv"
	"github.com/JetBrains/qodana-cli/internal/sarif"
	bbapi "github.com/reviewdog/go-bitbucket" // adapted from https://raw.githubusercontent.com/reviewdog/reviewdog/master/LICENSE
	log "github.com/sirupsen/logrus"
)

const (
	httpTimeout              = time.Second * 10
	bitBucketAnnotationLimit = 1000
	bitBucketReporter        = "JetBrains Qodana"
	bitBucketAvatar          = "https://avatars.githubusercontent.com/u/139879315"
	bitBucketReportFailed    = "FAILED"
	bitBucketReportPassed    = "PASSED"
	bitBucketReportType      = "BUG"
	bitBucketAnnotationType  = "CODE_SMELL"

	// https://developer.atlassian.com/cloud/bitbucket/rest/api-group-reports/#api-repositories-workspace-repo-slug-commit-commit-reports-reportid-annotations-annotationid-put-request
	bitBucketHigh   = "HIGH"
	bitBucketMedium = "MEDIUM"
	bitBucketLow    = "LOW"
	bitBucketInfo   = "INFO"

	pipelineProxyURL = "http://localhost:29418"
	pipeProxyURL     = "http://host.docker.internal:29418"
)

// toBitBucketSeverity maps SARIF and Qodana severity levels to BitBucket severity levels, levels are mapped to the closest match https://www.jetbrains.com/help/qodana/qodana-sarif-output.html#SARIF+severity
var (
	toBitBucketSeverity = map[string]string{
		sarifError:     bitBucketHigh,
		sarifWarning:   bitBucketMedium,
		sarifNote:      bitBucketLow,
		qodanaCritical: bitBucketHigh,
		qodanaHigh:     bitBucketHigh,
		qodanaModerate: bitBucketMedium,
		qodanaLow:      bitBucketLow,
		qodanaInfo:     bitBucketInfo,
	}
)

// sendBitBucketReport sends annotations to BitBucket code Insights
func sendBitBucketReport(annotations []bbapi.ReportAnnotation, toolName, cloudUrl, reportId string) error {
	client, ctx := getBitBucketClient(), getBitBucketContext()
	repoOwner, repoName, sha := qdenv.GetBitBucketRepoOwner(), qdenv.GetBitBucketRepoName(), qdenv.GetBitBucketCommit()
	_, resp, err := client.
		ReportsApi.CreateOrUpdateReport(ctx, repoOwner, repoName, sha, reportId).
		Body(buildReport(toolName, annotations, cloudUrl)).
		Execute()
	if err = checkBitBucketApiError(err, resp, http.StatusOK); err != nil {
		return fmt.Errorf("failed to create code insights report: %w", err)
	}
	totalAnnotations := len(annotations)
	if totalAnnotations != 0 {
		if totalAnnotations > bitBucketAnnotationLimit {
			totalAnnotations = bitBucketAnnotationLimit
			log.Debugf("Warning: Only first 1000 of %d annotations will be sent", len(annotations))
		}
		for i := 0; i < totalAnnotations; i += 100 {
			j := i + 100
			if j > totalAnnotations {
				j = totalAnnotations
			}
			_, resp, err := client.ReportsApi.
				BulkCreateOrUpdateAnnotations(ctx, repoOwner, repoName, sha, reportId).
				Body(annotations[i:j]).
				Execute()
			if err = checkBitBucketApiError(err, resp, http.StatusOK); err != nil {
				return fmt.Errorf("failed to create code insights annotations: %w", err)
			}
		}
	}
	return nil
}

// getBitBucketContext returns a context with BitBucket credentials (not required for runs in BitBucket Pipelines)
func getBitBucketContext() context.Context {
	ctx := context.Background()
	user, password, token :=
		os.Getenv("QD_BITBUCKET_USER"),
		os.Getenv("QD_BITBUCKET_PASSWORD"),
		os.Getenv("QD_BITBUCKET_TOKEN")
	if user != "" && password != "" {
		ctx = context.WithValue(
			ctx, bbapi.ContextBasicAuth,
			bbapi.BasicAuth{
				UserName: user,
				Password: password,
			},
		)
	}
	if token != "" {
		ctx = context.WithValue(ctx, bbapi.ContextAccessToken, token)
	}
	return ctx
}

// buildReport builds a report to be sent to BitBucket code Insights
func buildReport(toolName string, annotations []bbapi.ReportAnnotation, cloudUrl string) bbapi.Report {
	var result string
	if len(annotations) == 0 {
		result = bitBucketReportPassed
	} else {
		result = bitBucketReportFailed
	}

	data := bbapi.NewReport()
	data.SetTitle(toolName + " ")
	data.SetReportType(bitBucketReportType)
	data.SetReporter(bitBucketReporter)
	data.SetLogoUrl(bitBucketAvatar)
	data.SetLink(cloudUrl)
	data.SetDetails(msg.GetProblemsFoundMessage(len(annotations)))
	data.SetResult(result)
	return *data
}

// buildAnnotation builds an annotation to be sent to BitBucket code Insights
func buildAnnotation(r *sarif.Result, ruleDescription string, reportLink string) bbapi.ReportAnnotation {
	bbSeverity, ok := toBitBucketSeverity[getSeverity(r)]
	if !ok {
		log.Debugf("Unknown SARIF severity: %s", getSeverity(r))
		bbSeverity = bitBucketLow
	}

	data := bbapi.NewReportAnnotation()
	data.SetExternalId(getFingerprint(r))
	data.SetAnnotationType(bitBucketAnnotationType)
	data.SetSummary(fmt.Sprintf("%s: %s", r.RuleId, r.Message.Text))
	data.SetDetails(ruleDescription)
	data.SetSeverity(bbSeverity)

	if len(r.Locations) > 0 && r.Locations[0].PhysicalLocation != nil {
		location := r.Locations[0].PhysicalLocation
		if location.Region != nil {
			data.SetLine(int32(location.Region.StartLine))
		}
		if location.ArtifactLocation != nil {
			data.SetPath(location.ArtifactLocation.Uri)
		}
	}
	data.SetLink(reportLink)
	return *data
}

// getBitBucketClient returns a BitBucket API client with proper configuration by bbapi package
func getBitBucketClient() *bbapi.APIClient {
	config := bbapi.NewConfiguration()
	config.HTTPClient = &http.Client{
		Timeout: httpTimeout,
	}

	apiURL := os.Getenv("QD_BITBUCKET_URL")
	if apiURL == "" {
		if gitOrigin := os.Getenv("BITBUCKET_GIT_HTTP_ORIGIN"); gitOrigin != "" {
			if parsedURL, err := url.Parse(gitOrigin); err == nil {
				if !strings.Contains(parsedURL.Host, "bitbucket.org") {
					// Construct API URL for self-hosted BitBucket Data Center/Server
					// Reference: https://developer.atlassian.com/server/bitbucket/rest/v1000/intro/
					apiURL = fmt.Sprintf("%s://%s/rest/api/1.0", parsedURL.Scheme, parsedURL.Host)
				}
			}
		}
		if apiURL == "" {
			apiURL = "https://api.bitbucket.org/2.0"
		}
	}

	server := bbapi.ServerConfiguration{
		URL:         apiURL,
		Description: `HTTPS API endpoint`,
	}
	if qdenv.IsBitBucket() {
		var proxyURL *url.URL
		if qdenv.IsBitBucketPipe() {
			proxyURL, _ = url.Parse(pipeProxyURL)
		} else {
			proxyURL, _ = url.Parse(pipelineProxyURL)
		}
		config.HTTPClient.Transport = &http.Transport{
			Proxy: http.ProxyURL(proxyURL),
		}
		//goland:noinspection HttpUrlsUsage
		server = bbapi.ServerConfiguration{
			URL: "http://api.bitbucket.org/2.0",
		}
	}
	config.Servers = bbapi.ServerConfigurations{server}
	return bbapi.NewAPIClient(config)
}

// checkBitBucketApiError checks if the API call was successful
func checkBitBucketApiError(err error, resp *http.Response, expectedCode int) error {
	selfHostedHint := "Note: If you are using BitBucket Data Center/Server (self-hosted), " +
		"please set the QD_BITBUCKET_URL environment variable to your BitBucket API endpoint " +
		"(e.g., QD_BITBUCKET_URL=https://bitbucket.example.com/rest/api/1.0). " +
		"Read more at https://developer.atlassian.com/server/bitbucket/rest/v1000/intro/"

	if err != nil {
		return fmt.Errorf("BitBucket API error: %w%s", err, selfHostedHint)
	}
	if resp != nil && resp.StatusCode != expectedCode {
		body, _ := io.ReadAll(resp.Body)
		log.Debugf("Unexpected response: %s", body)
		return fmt.Errorf(
			"BitBucket API returned unexpected status code %d (expected %d)\n\n%s",
			resp.StatusCode, expectedCode, selfHostedHint,
		)
	}
	return nil
}
