package store

import (
	"context"
	"net/http"
	"strings"

	"github.com/doug-martin/goqu/v9"
	"github.com/jmoiron/sqlx"
	"golang.org/x/sync/errgroup"

	"github.com/pingcap/test-infra/common/model"
	reqM "github.com/pingcap/test-infra/common/model/tcms/request"
	serror "github.com/pingcap/test-infra/services/tcms/service/common/errors"
	"github.com/pingcap/test-infra/services/tcms/utils"
	"github.com/pingcap/test-infra/services/tcms/utils/sql"
)

type githubPlanService struct {
	db         *sql.Database
	planSvc    *planService
	triggerSvc *triggerService
}

func (s *githubPlanService) init(db *sql.Database, planSvc *planService, triggerSvc *triggerService) {
	s.db = db
	s.planSvc = planSvc
	s.triggerSvc = triggerSvc
}

func (s *githubPlanService) ListBranchNames(ctx utils.Context, prefix string) ([]string, error) {
	query := `SELECT name FROM gh_plan_branchs`
	args := []interface{}{}
	if prefix != "" {
		query += " WHERE name LIKE ?"
		args = append(args, sql.SanitizeLike(prefix)+"%")
	}

	branchs := []string{}
	err := s.db.SelectContext(ctx.Request().Context(), &branchs, query+" LIMIT 999", args...)
	return branchs, err
}

func (s *githubPlanService) ListBranchPlans(ctx utils.Context, branch string, planNames []string) ([]model.IDAndName, error) {
	where := goqu.Ex{}
	where["gh_branch"] = branch
	if len(planNames) > 0 {
		where["name"] = planNames
	}
	where["deleted_at"] = 0
	query, args, _ := sql.Builder.Select("id", "name").From("plans").Prepared(true).Where(where).Limit(999).ToSQL()

	plans := []model.IDAndName{}
	err := s.db.SelectContext(ctx.Request().Context(), &plans, query, args...)
	return plans, err
}

func (s *githubPlanService) getBranchLastCommitTs(ctx utils.Context, branch string) (*int64, error) {
	var lastCommitTs int64
	err := s.db.Get(&lastCommitTs, `SELECT last_commit_ts FROM gh_plan_branchs WHERE name = ?`, branch)
	if err != nil {
		if sql.IsErrNoRows(err) {
			return nil, nil
		}
		return nil, err
	}
	return &lastCommitTs, nil
}

func (s *githubPlanService) upsertBranch(ctx utils.Context, p *reqM.PlanCreateRequest) error {
	lastCommitTs, err := s.getBranchLastCommitTs(ctx, p.Branch)
	if err != nil {
		return err
	}
	if lastCommitTs != nil {
		if *lastCommitTs == p.CommitTs {
			// do nothing
			return nil
		} else if *lastCommitTs > p.CommitTs {
			return serror.NewMsg(http.StatusBadRequest, "commitTs is older than current, lastCommitTs= %d", *lastCommitTs)
		}
	}

	if ctx.DryRun() {
		return nil
	}

	// update `last_commit` if `lastCommitTs < newplan.CommitTs`
	_, err = s.db.Exec(`INSERT INTO gh_plan_branchs (name, last_commit, last_commit_ts) VALUES (?, ?, ?)
		ON DUPLICATE KEY UPDATE last_commit=VALUES(last_commit), last_commit_ts=VALUES(last_commit_ts)`,
		p.Branch,
		p.CommitHash,
		p.CommitTs,
	)
	return err
}

func (s *githubPlanService) upsertTrigger(ctx utils.Context, tx *sqlx.Tx, pid model.ID, p *reqM.PlanCreateRequest) error {
	t := &reqM.TriggerCreateRequest{
		Name:         model.TriggerNamePrefixGitHubPlan + p.Name,
		Creator:      p.Creator,
		ExecPriority: p.Priority,
		Type:         model.TriggerTypeCron,
		Active:       p.Schedule != "",
		Expr:         p.Schedule,
		PlanRef: model.PlanRef{
			PlanID:        pid,
			VariableBinds: []model.PlanVarBind{},
		},
	}

	ts, err := s.triggerSvc.parseTriggerExpr(model.TriggerTypeCron, t.Expr)
	if err != nil {
		ctx.Log().Warnf("parse trigger expr failed, err= %v", err)
		return err
	}
	res, err := tx.Exec(`UPDATE triggers SET name=?, priority=?, expr=?, ts=?, variables=?, active=?, updated_at=CURRENT_TIMESTAMP() WHERE plan_id = ? AND type = ?`,
		t.Name,
		t.ExecPriority,
		t.Expr,
		ts,
		t.VariableBinds,
		t.Active,
		t.PlanID,
		t.Type,
	)
	if err != nil {
		return err
	}
	rowsAffected, err := res.RowsAffected()
	if err != nil {
		return err
	} else if rowsAffected == 0 {
		// this is a new plan, create a cron trigger for it
		_, err = s.triggerSvc.create(ctx, tx, t)
		if err != nil {
			return err
		}
	}
	return nil
}

func (s *githubPlanService) Upsert(ctx utils.Context, p *reqM.PlanCreateRequest) (model.ID, error) {
	log := ctx.Log().With("planName", p.Name, "branch", p.Branch, "commit", p.CommitHash, "commitTs", p.CommitTs)
	log.Infof("upsert gh plan, path= %s", p.Path)

	g := errgroup.Group{}
	g.Go(func() (err error) {
		// record the newest commit
		return s.upsertBranch(ctx, p)
	})

	// Create inline cases and assign case id to steps
	pipeline := p.Pipeline
	p.IsSteps = len(p.Steps) > 0
	if p.IsSteps {
		pipeline = p.Steps
	}
	if err := s.planSvc.completeSteps(ctx, p.Name, p.InlineCases, pipeline); err != nil {
		return 0, err
	}
	if err := g.Wait(); err != nil {
		return 0, err
	}

	if ctx.DryRun() {
		return 0, nil
	}

	var id model.ID
	err := s.db.Transaction(context.Background(), func(tx *sqlx.Tx) error {
		// insert or update GitHub plan
		r, err := tx.Exec(`INSERT INTO plans (type, name, creator, test_bed, variables, meta_params, fail_fast, is_steps, gh_repo, gh_branch, gh_commit, gh_commit_ts, gh_path, gh_schedule, interrupt_action) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
			ON DUPLICATE KEY UPDATE creator=VALUES(creator), test_bed=VALUES(test_bed), variables=VALUES(variables), fail_fast=VALUES(fail_fast), is_steps=VALUES(is_steps), gh_repo=VALUES(gh_repo), gh_branch=VALUES(gh_branch), gh_commit=VALUES(gh_commit), gh_commit_ts=VALUES(gh_commit_ts), gh_path=VALUES(gh_path), gh_schedule=VALUES(gh_schedule), interrupt_action=VALUES(interrupt_action)`,
			p.Type,
			p.Name,
			p.Creator,
			p.TestBed,
			p.Variables,
			p.MetaParams,
			p.FailFast,
			p.IsSteps,
			p.Repo,
			p.Branch,
			p.CommitHash,
			p.CommitTs,
			p.Path,
			p.Schedule,
			p.InterruptAction,
		)
		if err != nil {
			log.Errorf(err, "CreateGHPlan failed")
			return err
		}

		rowsAffected, err := r.RowsAffected()
		if err != nil {
			return err
		}
		isNewPlan := rowsAffected == 1
		if isNewPlan {
			id_, err := r.LastInsertId()
			if err != nil {
				return err
			}
			id = model.ID(id_)
		} else {
			err = tx.Get(&id, "SELECT id FROM plans WHERE type = ? AND name = ? AND deleted_at = 0", p.Type, p.Name)
			if err != nil {
				return err
			}

			// delete old steps
			_, err = tx.Exec("DELETE FROM plan_steps WHERE plan_id=?", id)
			if err != nil {
				log.Errorf(err, "DeletePlan delete steps failed")
				return err
			}

			// delete old tags
			_, err = tx.Exec("DELETE FROM plan_tags WHERE plan_id=?", id)
			if err != nil {
				log.Errorf(err, "DeletePlan delete tags failed")
				return err
			}
		}

		// insert plan's steps
		if err := s.planSvc.insertSteps(tx, id, pipeline); err != nil {
			log.Errorf(err, "CreateGHPlan insert steps failed")
			return err
		}

		// insert plan's tags (GitHub Plan Only)
		if len(p.Tags) > 0 {
			query := strings.Builder{}
			args := []interface{}{}
			query.WriteString("INSERT INTO plan_tags (plan_id, name) VALUES (?, ?)")
			args = append(args, id, p.Tags[0])
			for i := 1; i < len(p.Tags); i++ {
				query.WriteString(",(?, ?)")
				args = append(args, id, p.Tags[i])
			}

			_, err = tx.Exec(query.String(), args...)
			if err != nil {
				log.Errorf(err, "CreatePlan insert tags failed")
				return err
			}
		}

		return s.upsertTrigger(ctx, tx, id, p)
	})

	return model.ID(id), err
}

func (s *githubPlanService) DeleteOlds(ctx utils.Context, branch string) error {
	log := ctx.Log().With("branch", branch)

	lastCommitTs, err := s.getBranchLastCommitTs(ctx, branch)
	if err != nil {
		return err
	} else if lastCommitTs == nil {
		log.Warnf("branch not found")
		return nil
	}

	var planIDs []model.ID
	err = s.db.Select(&planIDs, `SELECT id FROM plans WHERE gh_branch = ? AND gh_commit_ts < ? AND deleted_at=0`, branch, *lastCommitTs)
	if err != nil {
		return err
	}
	if len(planIDs) == 0 {
		log.Infof("no gh plans need to be deleted")
		return nil
	}

	log.Infof("these gh plan will be deleted: %v", planIDs)
	return s.planSvc.delete(ctx, planIDs, false)
}
