// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.

package gitrpc

import (
	"context"
	"errors"
	"fmt"
	"io"
	"path"

	"github.com/harness/gitness/gitrpc/internal/streamio"
	"github.com/harness/gitness/gitrpc/internal/types"
	"github.com/harness/gitness/gitrpc/rpc"

	"golang.org/x/sync/errgroup"
)

type DiffParams struct {
	ReadParams
	BaseRef      string
	HeadRef      string
	MergeBase    bool
	IncludePatch bool
}

func (p DiffParams) Validate() error {
	if err := p.ReadParams.Validate(); err != nil {
		return err
	}

	if p.HeadRef == "" {
		return ErrInvalidArgumentf("head ref cannot be empty")
	}
	return nil
}

func (c *Client) RawDiff(ctx context.Context, params *DiffParams, out io.Writer) error {
	if err := params.Validate(); err != nil {
		return err
	}
	diff, err := c.diffService.RawDiff(ctx, &rpc.DiffRequest{
		Base:      mapToRPCReadRequest(params.ReadParams),
		BaseRef:   params.BaseRef,
		HeadRef:   params.HeadRef,
		MergeBase: params.MergeBase,
	})
	if err != nil {
		return processRPCErrorf(err, "failed to fetch diff between '%s' and '%s' with err: %v",
			params.BaseRef, params.HeadRef, err)
	}

	reader := streamio.NewReader(func() ([]byte, error) {
		var resp *rpc.RawDiffResponse
		resp, err = diff.Recv()
		return resp.GetData(), err
	})

	if _, err = io.Copy(out, reader); err != nil {
		return fmt.Errorf("copy rpc data: %w", err)
	}

	return nil
}

type DiffShortStatOutput struct {
	Files     int
	Additions int
	Deletions int
}

// DiffShortStat returns files changed, additions and deletions metadata.
func (c *Client) DiffShortStat(ctx context.Context, params *DiffParams) (DiffShortStatOutput, error) {
	if err := params.Validate(); err != nil {
		return DiffShortStatOutput{}, err
	}
	stat, err := c.diffService.DiffShortStat(ctx, &rpc.DiffRequest{
		Base:      mapToRPCReadRequest(params.ReadParams),
		BaseRef:   params.BaseRef,
		HeadRef:   params.HeadRef,
		MergeBase: params.MergeBase,
	})
	if err != nil {
		return DiffShortStatOutput{}, processRPCErrorf(err, "failed to get diff data between '%s' and '%s'",
			params.BaseRef, params.HeadRef)
	}
	return DiffShortStatOutput{
		Files:     int(stat.GetFiles()),
		Additions: int(stat.GetAdditions()),
		Deletions: int(stat.GetDeletions()),
	}, nil
}

type DiffStatsOutput struct {
	Commits      int
	FilesChanged int
}

func (c *Client) DiffStats(ctx context.Context, params *DiffParams) (DiffStatsOutput, error) {
	// declare variables which will be used in go routines,
	// no need for atomic operations because writing and reading variable
	// doesn't happen at the same time
	var (
		totalCommits int
		totalFiles   int
	)

	errGroup, groupCtx := errgroup.WithContext(ctx)

	errGroup.Go(func() error {
		// read total commits

		options := &GetCommitDivergencesParams{
			ReadParams: params.ReadParams,
			Requests: []CommitDivergenceRequest{
				{
					From: params.HeadRef,
					To:   params.BaseRef,
				},
			},
		}

		rpcOutput, err := c.GetCommitDivergences(groupCtx, options)
		if err != nil {
			return processRPCErrorf(err, "failed to count pull request commits between '%s' and '%s'",
				params.BaseRef, params.HeadRef)
		}
		if len(rpcOutput.Divergences) > 0 {
			totalCommits = int(rpcOutput.Divergences[0].Ahead)
		}
		return nil
	})

	errGroup.Go(func() error {
		// read short stat
		stat, err := c.DiffShortStat(groupCtx, &DiffParams{
			ReadParams: params.ReadParams,
			BaseRef:    params.BaseRef,
			HeadRef:    params.HeadRef,
			MergeBase:  true, // must be true, because commitDivergences use tripple dot notation
		})
		if err != nil {
			return err
		}
		totalFiles = stat.Files
		return nil
	})

	err := errGroup.Wait()
	if err != nil {
		return DiffStatsOutput{}, err
	}

	return DiffStatsOutput{
		Commits:      totalCommits,
		FilesChanged: totalFiles,
	}, nil
}

type GetDiffHunkHeadersParams struct {
	ReadParams
	SourceCommitSHA string
	TargetCommitSHA string
}

type DiffFileHeader struct {
	OldName    string
	NewName    string
	Extensions map[string]string
}

type HunkHeader struct {
	OldLine int
	OldSpan int
	NewLine int
	NewSpan int
	Text    string
}

type DiffFileHunkHeaders struct {
	FileHeader  DiffFileHeader
	HunkHeaders []HunkHeader
}

type GetDiffHunkHeadersOutput struct {
	Files []DiffFileHunkHeaders
}

func (c *Client) GetDiffHunkHeaders(
	ctx context.Context,
	params GetDiffHunkHeadersParams,
) (GetDiffHunkHeadersOutput, error) {
	if params.SourceCommitSHA == params.TargetCommitSHA {
		return GetDiffHunkHeadersOutput{}, nil
	}

	hunkHeaders, err := c.diffService.GetDiffHunkHeaders(ctx, &rpc.GetDiffHunkHeadersRequest{
		Base:            mapToRPCReadRequest(params.ReadParams),
		SourceCommitSha: params.SourceCommitSHA,
		TargetCommitSha: params.TargetCommitSHA,
	})
	if err != nil {
		return GetDiffHunkHeadersOutput{}, processRPCErrorf(err, "failed to get git diff hunk headers")
	}

	files := make([]DiffFileHunkHeaders, len(hunkHeaders.Files))
	for i, file := range hunkHeaders.Files {
		headers := make([]HunkHeader, len(file.HunkHeaders))
		for j, header := range file.HunkHeaders {
			headers[j] = mapHunkHeader(header)
		}
		files[i] = DiffFileHunkHeaders{
			FileHeader:  mapDiffFileHeader(file.FileHeader),
			HunkHeaders: headers,
		}
	}

	return GetDiffHunkHeadersOutput{
		Files: files,
	}, nil
}

type DiffCutOutput struct {
	Header          HunkHeader
	LinesHeader     string
	Lines           []string
	MergeBaseSHA    string
	LatestSourceSHA string
}

type DiffCutParams struct {
	ReadParams
	SourceCommitSHA string
	SourceBranch    string
	TargetCommitSHA string
	TargetBranch    string
	Path            string
	LineStart       int
	LineStartNew    bool
	LineEnd         int
	LineEndNew      bool
}

// DiffCut extracts diff snippet from a git diff hunk.
// The snippet is from the specific commit (specified by commit SHA), between refs
// source branch and target branch, from the specific file.
func (c *Client) DiffCut(ctx context.Context, params *DiffCutParams) (DiffCutOutput, error) {
	result, err := c.diffService.DiffCut(ctx, &rpc.DiffCutRequest{
		Base:            mapToRPCReadRequest(params.ReadParams),
		SourceCommitSha: params.SourceCommitSHA,
		SourceBranch:    params.SourceBranch,
		TargetCommitSha: params.TargetCommitSHA,
		TargetBranch:    params.TargetBranch,
		Path:            params.Path,
		LineStart:       int32(params.LineStart),
		LineStartNew:    params.LineStartNew,
		LineEnd:         int32(params.LineEnd),
		LineEndNew:      params.LineEndNew,
	})
	if err != nil {
		return DiffCutOutput{}, processRPCErrorf(err, "failed to get git diff sub hunk")
	}

	hunkHeader := types.HunkHeader{
		OldLine: int(result.HunkHeader.OldLine),
		OldSpan: int(result.HunkHeader.OldSpan),
		NewLine: int(result.HunkHeader.NewLine),
		NewSpan: int(result.HunkHeader.NewSpan),
		Text:    result.HunkHeader.Text,
	}

	return DiffCutOutput{
		Header:          HunkHeader(hunkHeader),
		LinesHeader:     result.LinesHeader,
		Lines:           result.Lines,
		MergeBaseSHA:    result.MergeBaseSha,
		LatestSourceSHA: result.LatestSourceSha,
	}, nil
}

type FileDiff struct {
	SHA         string `json:"sha"`
	OldSHA      string `json:"old_sha,omitempty"`
	Path        string `json:"path"`
	OldPath     string `json:"old_path,omitempty"`
	Status      string `json:"status"`
	Additions   int64  `json:"additions"`
	Deletions   int64  `json:"deletions"`
	Changes     int64  `json:"changes"`
	ContentURL  string `json:"content_url"`
	Patch       []byte `json:"patch,omitempty"`
	IsBinary    bool   `json:"is_binary"`
	IsSubmodule bool   `json:"is_submodule"`
}

func (c *Client) Diff(ctx context.Context, params *DiffParams, baseURL string) (<-chan *FileDiff, <-chan error) {
	ch := make(chan *FileDiff)
	// needs to be buffered so it is not blocking on receiver side when all data is sent
	cherr := make(chan error, 1)

	go func() {
		defer close(ch)
		defer close(cherr)

		if err := params.Validate(); err != nil {
			cherr <- err
			return
		}

		stream, err := c.diffService.Diff(ctx, &rpc.DiffRequest{
			Base:         mapToRPCReadRequest(params.ReadParams),
			BaseRef:      params.BaseRef,
			HeadRef:      params.HeadRef,
			MergeBase:    params.MergeBase,
			IncludePatch: params.IncludePatch,
		})
		if err != nil {
			return
		}

		for {
			resp, err := stream.Recv()
			if err != nil {
				if errors.Is(err, io.EOF) {
					break
				}
				cherr <- processRPCErrorf(err, "failed to get git diff file from stream")
				return
			}

			ch <- &FileDiff{
				SHA:         resp.Sha,
				OldSHA:      resp.OldSha,
				Path:        resp.Path,
				OldPath:     resp.OldPath,
				Status:      resp.Status.String(),
				Additions:   int64(resp.Additions),
				Deletions:   int64(resp.Deletions),
				Changes:     int64(resp.Changes),
				ContentURL:  path.Join(baseURL, resp.Path),
				Patch:       resp.Patch,
				IsBinary:    resp.IsBinary,
				IsSubmodule: resp.IsSubmodule,
			}
		}
	}()

	return ch, cherr
}