// Copyright 2023 Harness, Inc.
//
// 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
//
//     http://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 importer

import (
	"context"
	"fmt"
	"path"
	"strings"
	"time"

	"github.com/harness/gitness/git"
	"github.com/harness/gitness/git/sha"
	"github.com/harness/gitness/store/database/dbtx"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/types/enum"

	"github.com/drone/go-convert/convert/bitbucket"
	"github.com/drone/go-convert/convert/circle"
	"github.com/drone/go-convert/convert/drone"
	"github.com/drone/go-convert/convert/github"
	"github.com/drone/go-convert/convert/gitlab"
	"github.com/rs/zerolog/log"
)

type pipelineFile struct {
	Name          string
	OriginalPath  string
	ConvertedPath string
	Content       []byte
}

func (r *Repository) processPipelines(ctx context.Context,
	principal *types.Principal,
	repo *types.Repository,
	commitMessage string,
) error {
	writeParams, err := r.createRPCWriteParams(ctx, principal, repo)
	if err != nil {
		return err
	}

	pipelineFiles := r.convertPipelines(ctx, repo)
	if len(pipelineFiles) == 0 {
		return nil
	}

	actions := make([]git.CommitFileAction, len(pipelineFiles))
	for i, file := range pipelineFiles {
		actions[i] = git.CommitFileAction{
			Action:  git.CreateAction,
			Path:    file.ConvertedPath,
			Payload: file.Content,
			SHA:     sha.None,
		}
	}

	now := time.Now()
	identity := &git.Identity{
		Name:  principal.DisplayName,
		Email: principal.Email,
	}

	_, err = r.git.CommitFiles(ctx, &git.CommitFilesParams{
		WriteParams:   writeParams,
		Title:         commitMessage,
		Message:       "",
		Branch:        repo.DefaultBranch,
		NewBranch:     repo.DefaultBranch,
		Actions:       actions,
		Committer:     identity,
		CommitterDate: &now,
		Author:        identity,
		AuthorDate:    &now,
	})
	if err != nil {
		return fmt.Errorf("failed to commit converted pipeline files: %w", err)
	}

	nowMilli := now.UnixMilli()

	err = r.tx.WithTx(ctx, func(ctx context.Context) error {
		for _, p := range pipelineFiles {
			pipeline := &types.Pipeline{
				Description:   "",
				RepoID:        repo.ID,
				Identifier:    p.Name,
				CreatedBy:     principal.ID,
				Seq:           0,
				DefaultBranch: repo.DefaultBranch,
				ConfigPath:    p.ConvertedPath,
				Created:       nowMilli,
				Updated:       nowMilli,
				Version:       0,
			}

			err = r.pipelineStore.Create(ctx, pipeline)
			if err != nil {
				return fmt.Errorf("pipeline creation failed: %w", err)
			}

			// Try to create a default trigger on pipeline creation.
			// Default trigger operations are set on pull request created, reopened or updated.
			// We log an error on failure but don't fail the op.
			trigger := &types.Trigger{
				Description: "auto-created trigger on pipeline conversion",
				Created:     nowMilli,
				Updated:     nowMilli,
				PipelineID:  pipeline.ID,
				RepoID:      pipeline.RepoID,
				CreatedBy:   principal.ID,
				Identifier:  "default",
				Actions: []enum.TriggerAction{enum.TriggerActionPullReqCreated,
					enum.TriggerActionPullReqReopened, enum.TriggerActionPullReqBranchUpdated},
				Disabled: false,
				Version:  0,
			}
			err = r.triggerStore.Create(ctx, trigger)
			if err != nil {
				return fmt.Errorf("failed to create auto trigger on pipeline creation: %w", err)
			}
		}

		return nil
	}, dbtx.TxDefault)
	if err != nil {
		return fmt.Errorf("failed to insert pipelines and triggers: %w", err)
	}

	return nil
}

// convertPipelines converts pipelines found in the repository.
// Note: For GitHub actions, there can be multiple.
func (r *Repository) convertPipelines(ctx context.Context,
	repo *types.Repository,
) []pipelineFile {
	const maxSize = 65536

	match := func(dirPath, regExpDef string) []pipelineFile {
		files, err := r.matchFiles(ctx, repo, repo.DefaultBranch, dirPath, regExpDef, maxSize)
		if err != nil {
			log.Ctx(ctx).Warn().Err(err).Msgf("failed to find pipeline file(s) '%s' in '%s'",
				regExpDef, dirPath)
			return nil
		}
		return files
	}

	if files := match("", ".drone.yml"); len(files) > 0 {
		converted := convertPipelineFiles(ctx, files, func() pipelineConverter { return drone.New() })
		if len(converted) > 0 {
			return converted
		}
	}

	if files := match("", "bitbucket-pipelines.yml"); len(files) > 0 {
		converted := convertPipelineFiles(ctx, files, func() pipelineConverter { return bitbucket.New() })
		if len(converted) > 0 {
			return converted
		}
	}

	if files := match("", ".gitlab-ci.yml"); len(files) > 0 {
		converted := convertPipelineFiles(ctx, files, func() pipelineConverter { return gitlab.New() })
		if len(converted) > 0 {
			return converted
		}
	}

	if files := match(".circleci", "config.yml"); len(files) > 0 {
		converted := convertPipelineFiles(ctx, files, func() pipelineConverter { return circle.New() })
		if len(converted) > 0 {
			return converted
		}
	}

	filesYML := match(".github/workflows", "*.yml")
	filesYAML := match(".github/workflows", "*.yaml")
	//nolint:gocritic // intended usage
	files := append(filesYML, filesYAML...)
	converted := convertPipelineFiles(ctx, files, func() pipelineConverter { return github.New() })
	if len(converted) > 0 {
		return converted
	}

	return nil
}

type pipelineConverter interface {
	ConvertBytes([]byte) ([]byte, error)
}

func convertPipelineFiles(ctx context.Context,
	files []pipelineFile,
	gen func() pipelineConverter,
) []pipelineFile {
	const (
		harnessPipelineName     = "pipeline"
		harnessPipelineNameOnly = "default-" + harnessPipelineName
		harnessPipelineDir      = ".harness"
		harnessPipelineFileOnly = harnessPipelineDir + "/pipeline.yaml"
	)

	result := make([]pipelineFile, 0, len(files))
	for _, file := range files {
		data, err := gen().ConvertBytes(file.Content)
		if err != nil {
			log.Ctx(ctx).Warn().Err(err).Msgf("failed to convert pipeline file %s", file.OriginalPath)
			continue
		}

		var pipelineName string
		var pipelinePath string

		if len(files) == 1 {
			pipelineName = harnessPipelineNameOnly
			pipelinePath = harnessPipelineFileOnly
		} else {
			base := path.Base(file.OriginalPath)
			base = strings.TrimSuffix(base, path.Ext(base))
			pipelineName = harnessPipelineName + "-" + base
			pipelinePath = harnessPipelineDir + "/" + base + ".yaml"
		}

		result = append(result, pipelineFile{
			Name:          pipelineName,
			OriginalPath:  file.OriginalPath,
			ConvertedPath: pipelinePath,
			Content:       data,
		})
	}

	return result
}
