/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements.  See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 tasks

import (
	"context"
	"reflect"

	"github.com/apache/incubator-devlake/core/dal"
	"github.com/apache/incubator-devlake/core/errors"
	"github.com/apache/incubator-devlake/core/models/domainlayer/code"
	"github.com/apache/incubator-devlake/core/plugin"
	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
	"github.com/apache/incubator-devlake/plugins/refdiff/models"
	"github.com/apache/incubator-devlake/plugins/refdiff/utils"
)

var CalculateDeploymentCommitsDiffMeta = plugin.SubTaskMeta{
	Name:             "calculateDeploymentCommitsDiff",
	EntryPoint:       CalculateDeploymentCommitsDiff,
	EnabledByDefault: true,
	Description:      "Calculate commits diff between deployments in the specified project",
	DomainTypes:      []string{plugin.DOMAIN_TYPE_CODE},
}

func CalculateDeploymentCommitsDiff(taskCtx plugin.SubTaskContext) errors.Error {
	data := taskCtx.GetData().(*RefdiffTaskData)
	db := taskCtx.GetDal()
	ctx := taskCtx.GetContext()
	logger := taskCtx.GetLogger()

	if data.Options.ProjectName == "" {
		return nil
	}

	// step 1. select all deployment commits that need to be calculated
	pairs := make([]*deploymentCommitPair, 0)
	err := db.All(
		&pairs,
		dal.Select("dc.id, dc.commit_sha, p.commit_sha as prev_commit_sha"),
		dal.From("cicd_deployment_commits dc"),
		dal.Join("LEFT JOIN project_mapping pm ON (pm.table = 'cicd_scopes' AND pm.row_id = dc.cicd_scope_id)"),
		dal.Join("LEFT JOIN cicd_deployment_commits p ON (dc.prev_success_deployment_commit_id = p.id)"),
		dal.Where(
			`
			pm.project_name = ?
			AND NOT EXISTS (
				SELECT 1
				FROM _tool_refdiff_finished_commits_diffs fcd
				WHERE fcd.new_commit_sha = dc.commit_sha AND fcd.old_commit_sha = p.commit_sha
			)
			`,
			data.Options.ProjectName,
		),
		dal.Orderby(`dc.cicd_scope_id, dc.repo_url, dc.environment, dc.started_date`),
	)
	if err != nil {
		return err
	}
	pairsCount := len(pairs)
	if pairsCount == 0 {
		// graph is expensive, we should avoid creating one for nothing
		return nil
	}

	// step 2. construct a commit node graph and batch save
	graph, err := loadCommitGraph(ctx, db, data)
	if err != nil {
		return err
	}
	batch_save, err := api.NewBatchSave(taskCtx, reflect.TypeOf(&code.CommitsDiff{}), 1000)
	if err != nil {
		return err
	}

	// step 3. iterate all pairs and calculate diff
	taskCtx.SetProgress(0, pairsCount)
	for _, pair := range pairs {
		select {
		case <-ctx.Done():
			return errors.Convert(ctx.Err())
		default:
		}
		lostSha, oldCount, newCount := graph.CalculateLostSha(pair.PrevCommitSha, pair.CommitSha)
		for i, sha := range lostSha {
			commitsDiff := &code.CommitsDiff{
				NewCommitSha: pair.CommitSha,
				OldCommitSha: pair.PrevCommitSha,
				CommitSha:    sha,
				SortingIndex: i + 1,
			}
			err = batch_save.Add(commitsDiff)
			if err != nil {
				return err
			}
		}
		err = batch_save.Flush()
		if err != nil {
			return err
		}
		// mark commits_diff were calculated, no need to do it again in the future
		finishedCommitsDiff := &models.FinishedCommitsDiff{
			NewCommitSha: pair.CommitSha,
			OldCommitSha: pair.PrevCommitSha,
		}
		err = db.CreateOrUpdate(finishedCommitsDiff)
		if err != nil {
			return err
		}

		logger.Info(
			"total %d commits of difference found between [new][%s] and [old][%s(total:%d)]",
			newCount,
			pair.CommitSha,
			pair.PrevCommitSha,
			oldCount,
		)
		taskCtx.IncProgress(1)
	}
	return nil
}

type deploymentCommitPair struct {
	Id            string
	CommitSha     string
	PrevCommitSha string
}

func loadCommitGraph(ctx context.Context, db dal.Dal, data *RefdiffTaskData) (*utils.CommitNodeGraph, errors.Error) {
	graph := utils.NewCommitNodeGraph()

	cursor, err := db.Cursor(
		dal.Select("cp.commit_sha, cp.parent_commit_sha"),
		dal.From("commit_parents cp"),
		dal.Join("LEFT JOIN repo_commits rc ON (rc.commit_sha = cp.commit_sha)"),
		dal.Join("LEFT JOIN project_mapping pm ON (pm.table = 'repos' AND pm.row_id = rc.repo_id)"),
		dal.Where("pm.project_name = ?", data.Options.ProjectName),
	)
	if err != nil {
		return nil, err
	}
	defer cursor.Close()

	for cursor.Next() {
		select {
		case <-ctx.Done():
			return nil, errors.Convert(ctx.Err())
		default:
		}
		commitParent := &code.CommitParent{}
		err = db.Fetch(cursor, commitParent)
		if err != nil {
			return nil, err
		}
		graph.AddParent(commitParent.CommitSha, commitParent.ParentCommitSha)
	}

	return graph, nil
}
