/*
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 service

import (
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	"github.com/apache/incubator-devlake/core/dal"
	"github.com/apache/incubator-devlake/core/errors"
	"github.com/apache/incubator-devlake/core/models/common"
	"github.com/apache/incubator-devlake/core/models/domainlayer"
	"github.com/apache/incubator-devlake/core/models/domainlayer/crossdomain"
	"github.com/apache/incubator-devlake/core/models/domainlayer/qa"
	"github.com/apache/incubator-devlake/core/models/domainlayer/ticket"
	"github.com/apache/incubator-devlake/helpers/pluginhelper"
	customizeModels "github.com/apache/incubator-devlake/plugins/customize/models"
)

// Service wraps database operations
type Service struct {
	dal         dal.Dal
	nameChecker *regexp.Regexp
}

func NewService(dal dal.Dal) *Service {
	return &Service{dal: dal, nameChecker: regexp.MustCompile(`^x_[a-zA-Z0-9_]{0,50}$`)}
}

// GetFields returns all the fields of the table
func (s *Service) GetFields(table string) ([]customizeModels.CustomizedField, errors.Error) {
	// the customized fields created before v0.16.0 were not recorded in the table `_tool_customized_field`, we should take care of them
	columns, err := s.dal.GetColumns(&customizeModels.Table{Name: table}, func(columnMeta dal.ColumnMeta) bool {
		return true
	})
	if err != nil {
		return nil, errors.Default.Wrap(err, "GetColumns error")
	}
	ff, err := s.getCustomizedFields(table)
	if err != nil {
		return nil, err
	}
	fieldMap := make(map[string]customizeModels.CustomizedField)
	for _, f := range ff {
		fieldMap[f.ColumnName] = f
	}
	var result []customizeModels.CustomizedField
	for _, col := range columns {
		// original fields
		if !strings.HasPrefix(col.Name(), "x_") {
			dataType, _ := col.ColumnType()
			result = append(result, customizeModels.CustomizedField{
				TbName:     table,
				ColumnName: col.Name(),
				DataType:   dal.ColumnType(dataType),
			})
			// customized fields
		} else {
			if field, ok := fieldMap[col.Name()]; ok {
				result = append(result, field)
			} else {
				result = append(result, customizeModels.CustomizedField{
					ColumnName: col.Name(),
					DataType:   dal.Varchar,
				})
			}
		}
	}
	return result, nil
}

// checkField checks if the field exist in table
func (s *Service) checkField(table, field string) (bool, errors.Error) {
	if table == "" {
		return false, errors.Default.New("empty table name")
	}
	if !strings.HasPrefix(field, "x_") {
		return false, errors.Default.New("column name should start with `x_`")
	}
	if !s.nameChecker.MatchString(field) {
		return false, errors.Default.New("invalid column name")
	}
	fields, err := s.GetFields(table)
	if err != nil {
		return false, err
	}
	for _, fld := range fields {
		if fld.ColumnName == field {
			return true, nil
		}
	}
	return false, nil
}

// CreateField creates a new column for the table cf.TbName and creates a new record in the table `_tool_customized_fields`
func (s *Service) CreateField(cf *customizeModels.CustomizedField) errors.Error {
	exists, err := s.checkField(cf.TbName, cf.ColumnName)
	if err != nil {
		return err
	}
	if exists {
		return errors.BadInput.New(fmt.Sprintf("the column %s already exists", cf.ColumnName))
	}
	err = s.dal.Create(cf)
	if err != nil {
		return errors.Default.Wrap(err, "create customizedField")
	}
	err = s.dal.AddColumn(cf.TbName, cf.ColumnName, cf.DataType)
	if err != nil {
		return errors.Default.Wrap(err, "AddColumn error")
	}
	return nil
}

// DeleteField deletes the `field` form the `table`
func (s *Service) DeleteField(table, field string) errors.Error {
	exists, err := s.checkField(table, field)
	if err != nil {
		return err
	}
	if !exists {
		return nil
	}
	err = s.dal.DropColumns(table, field)
	if err != nil {
		return errors.Default.Wrap(err, "DropColumn error")
	}
	return s.dal.Delete(&customizeModels.CustomizedField{}, dal.Where("tb_name = ? AND column_name = ?", table, field))
}

// getCustomizedFields returns all the customized fields definitions of the table
func (s *Service) getCustomizedFields(table string) ([]customizeModels.CustomizedField, errors.Error) {
	var result []customizeModels.CustomizedField
	err := s.dal.All(&result, dal.Where("tb_name = ?", table))
	return result, err
}

// ImportIssue import csv file to the table `issues`, and create relations to boards
// issue could exist in multiple boards, so we should only delete an old records when it doesn't belong to another board
func (s *Service) ImportIssue(boardId string, file io.ReadCloser, incremental bool) errors.Error {
	if !incremental {
		// not delete accounts data since account may be referenced by others
		err := s.dal.Delete(
			&ticket.Issue{},
			dal.Where("id IN (SELECT issue_id FROM board_issues WHERE board_id=? AND issue_id NOT IN (SELECT issue_id FROM board_issues WHERE board_id!=?))", boardId, boardId),
		)
		if err != nil {
			return err
		}

		err = s.dal.Delete(
			&ticket.IssueLabel{},
			dal.Where("issue_id IN (SELECT issue_id FROM board_issues WHERE board_id=? AND issue_id NOT IN (SELECT issue_id FROM board_issues WHERE board_id!=?))", boardId, boardId),
		)
		if err != nil {
			return err
		}

		err = s.dal.Delete(
			&ticket.BoardIssue{},
			dal.Where("board_id = ?", boardId),
		)
		if err != nil {
			return err
		}
	}
	return s.importCSV(file, boardId, s.issueHandlerFactory(boardId, incremental))
}

// SaveBoard make sure the board exists in table `boards`
func (s *Service) SaveBoard(boardId, boardName string) errors.Error {
	return s.dal.CreateOrUpdate(&ticket.Board{
		DomainEntity: domainlayer.DomainEntity{
			Id: boardId,
		},
		Name: boardName,
		Type: "csv",
	})
}

// ImportIssueCommit imports csv file into the table `issue_commits`
func (s *Service) ImportIssueCommit(boardId string, file io.ReadCloser) errors.Error {
	err := s.dal.Delete(
		&crossdomain.IssueCommit{},
		dal.Where("issue_id IN (SELECT issue_id FROM board_issues WHERE board_id=? AND issue_id NOT IN (SELECT issue_id FROM board_issues WHERE board_id!=?))", boardId, boardId),
	)
	if err != nil {
		return err
	}
	return s.importCSV(file, boardId, s.issueCommitHandler)
}

// ImportIssueRepoCommit imports data to the table `issue_repo_commits` and `issue_commits`
func (s *Service) ImportIssueRepoCommit(boardId string, file io.ReadCloser, incremental bool) errors.Error {
	if !incremental {
		// delete old records of the table `issue_repo_commit` and `issue_commit`
		err := s.dal.Delete(
			&crossdomain.IssueRepoCommit{},
			dal.Where("issue_id IN (SELECT issue_id FROM board_issues WHERE board_id=? AND issue_id NOT IN (SELECT issue_id FROM board_issues WHERE board_id!=?))", boardId, boardId),
		)
		if err != nil {
			return err
		}
		err = s.dal.Delete(
			&crossdomain.IssueCommit{},
			dal.Where("issue_id IN (SELECT issue_id FROM board_issues WHERE board_id=? AND issue_id NOT IN (SELECT issue_id FROM board_issues WHERE board_id!=?))", boardId, boardId),
		)
		if err != nil {
			return err
		}
	}
	return s.importCSV(file, boardId, s.issueRepoCommitHandler)
}

// importCSV extract records from csv file, and save them to DB using recordHandler
// the rawDataParams is used to identify the data source,
// the recordHandler is used to handle the record, it should return an error if the record is invalid
// the `created_at` and `updated_at` will be set to the current time
func (s *Service) importCSV(file io.ReadCloser, rawDataParams string, recordHandler func(map[string]interface{}) errors.Error) errors.Error {
	iterator, err := pluginhelper.NewCsvFileIteratorFromFile(file)
	if err != nil {
		return err
	}
	var hasNext bool
	var line int
	now := time.Now()
	for {
		line++
		if hasNext, err = iterator.HasNextWithError(); !hasNext {
			return errors.BadInput.Wrap(err, fmt.Sprintf("error on processing the line:%d", line))
		} else {
			record := iterator.Fetch()
			record["_raw_data_params"] = rawDataParams
			for k, v := range record {
				if v.(string) == "NULL" {
					record[k] = nil
				}
			}
			record["created_at"] = now
			record["updated_at"] = now
			err = recordHandler(record)
			if err != nil {
				return errors.BadInput.Wrap(err, fmt.Sprintf("error on processing the line:%d", line))
			}
		}
	}
}

// createOrUpdateAccount creates or updates an account based on the provided name.
// It returns the account ID and an error if any occurred.
func (s *Service) createOrUpdateAccount(accountName string, rawDataParams string) (string, errors.Error) {
	if accountName == "" {
		return "", nil // Return empty ID if name is empty, no error needed here.
	}
	now := time.Now()
	accountId := fmt.Sprintf("csv:CsvAccount:0:%s", accountName)
	account := &crossdomain.Account{
		DomainEntity: domainlayer.DomainEntity{
			Id: accountId,
			NoPKModel: common.NoPKModel{
				RawDataOrigin: common.RawDataOrigin{
					RawDataParams: rawDataParams,
				},
			},
		},
		FullName:    accountName,
		UserName:    accountName,
		CreatedDate: &now, // FIXME: will update created_date if already exists. to debug, using created_at instead
	}
	err := s.dal.CreateOrUpdate(account)
	if err != nil {
		return "", errors.Default.Wrap(err, fmt.Sprintf("failed to create or update account for %s", accountName))
	}
	return accountId, nil
}

// getStringField extracts a string field from a record map.
// If required is true, it returns an error if the field is missing, nil, empty, or not a string.
// If required is false, it returns an empty string without error if the field is missing or nil,
// but returns an error if the field exists and is not a string.
func getStringField(record map[string]interface{}, fieldName string, required bool) (string, errors.Error) {
	value, ok := record[fieldName]
	if !ok || value == nil {
		if required {
			return "", errors.Default.New(fmt.Sprintf("record without required field %s", fieldName))
		}
		return "", nil // Field missing or nil, but not required
	}

	strValue, ok := value.(string)
	if !ok {
		return "", errors.Default.New(fmt.Sprintf("%s is not a string", fieldName))
	}

	if required && strValue == "" {
		return "", errors.Default.New(fmt.Sprintf("invalid or empty required field %s", fieldName))
	}

	return strValue, nil
}

// issueHandlerFactory returns a handler that save record into `issues`, `board_issues` and `issue_labels` table
func (s *Service) issueHandlerFactory(boardId string, incremental bool) func(record map[string]interface{}) errors.Error {
	return func(record map[string]interface{}) errors.Error {
		var err errors.Error
		id, err := getStringField(record, "id", true)
		if err != nil {
			return err
		}

		// Handle labels
		labels, err := getStringField(record, "labels", false)
		if err != nil {
			return err
		}
		if labels != "" {
			var issueLabels []*ticket.IssueLabel
			appearedLabels := make(map[string]struct{}) // record the labels that have appeared
			for _, label := range strings.Split(labels, ",") {
				label = strings.TrimSpace(label)
				if label == "" {
					continue
				}
				if _, appeared := appearedLabels[label]; !appeared {
					issueLabel := &ticket.IssueLabel{
						IssueId:   id,
						LabelName: label,
						NoPKModel: common.NoPKModel{
							RawDataOrigin: common.RawDataOrigin{
								RawDataParams: boardId,
							},
						},
					}
					issueLabels = append(issueLabels, issueLabel)
					appearedLabels[label] = struct{}{}
				}
			}
			if len(issueLabels) > 0 {
				err = s.dal.CreateOrUpdate(issueLabels)
				if err != nil {
					return err
				}
			}
		}
		delete(record, "labels") // Remove labels from record map as it's handled

		// Handle creator and assignee accounts
		rawDataParams, err := getStringField(record, "_raw_data_params", true)
		if err != nil {
			// This should ideally not happen as it's set in importCSV, but good to check
			return err
		}

		// Handle creator
		creatorName, err := getStringField(record, "creator_name", false)
		if err != nil {
			return err
		}
		creatorId, err := s.createOrUpdateAccount(creatorName, rawDataParams)
		if err != nil {
			return err
		}
		if creatorId != "" {
			record["creator_id"] = creatorId
		}

		// Handle assignee
		assigneeName, err := getStringField(record, "assignee_name", false)
		if err != nil {
			return err
		}
		assigneeId, err := s.createOrUpdateAccount(assigneeName, rawDataParams)
		if err != nil {
			return err
		}
		if assigneeId != "" {
			record["assignee_id"] = assigneeId
		}

		// Handle sprint_ids
		sprintIds, err := getStringField(record, "sprint_ids", false)
		if err != nil {
			return err
		}
		sprints := strings.Split(strings.TrimSpace(sprintIds), ",")
		for _, sprintId := range sprints {
			sprintId = strings.TrimSpace(sprintId)
			if sprintId != "" {
				err = s.dal.CreateOrUpdate(&ticket.SprintIssue{
					SprintId: sprintId,
					IssueId:  id,
				})
				if err != nil {
					return err
				}
			}
		}
		delete(record, "sprint_ids")

		// Handle issues
		err = s.dal.CreateWithMap(&ticket.Issue{}, record)
		if err != nil {
			return err
		}

		// Handle board_issues
		err = s.dal.CreateOrUpdate(&ticket.BoardIssue{
			BoardId: boardId,
			IssueId: id,
		})
		if err != nil {
			return err
		}
		return nil
	}
}

// issueCommitHandler save record into `issue_commits` table
func (s *Service) issueCommitHandler(record map[string]interface{}) errors.Error {
	return s.dal.CreateWithMap(&crossdomain.IssueCommit{}, record)
}

// ImportQaApis imports csv file to the table `qa_apis`
func (s *Service) ImportQaApis(qaProjectId string, file io.ReadCloser, incremental bool) errors.Error {
	if !incremental {
		// delete old data associated with this qaProjectId
		err := s.dal.Delete(&qa.QaApi{}, dal.Where("qa_project_id = ?", qaProjectId))
		if err != nil {
			return errors.Default.Wrap(err, fmt.Sprintf("failed to delete old qa_apis for qaProjectId %s", qaProjectId))
		}
	}
	return s.importCSV(file, qaProjectId, s.qaApiHandler(qaProjectId))
}

// qaApiHandler saves a record into the `qa_apis` table
func (s *Service) qaApiHandler(qaProjectId string) func(record map[string]interface{}) errors.Error {
	return func(record map[string]interface{}) errors.Error {
		creatorName, err := getStringField(record, "creator_name", false)
		if err != nil {
			return err
		}
		if creatorName != "" {
			creatorId, _ := s.createOrUpdateAccount(creatorName, qaProjectId)
			if creatorId != "" {
				record["creator_id"] = creatorId
			}
		}
		delete(record, "creator_name")
		record["qa_project_id"] = qaProjectId
		return s.dal.CreateWithMap(&qa.QaApi{}, record)
	}
}

// ImportQaTestCases imports csv file to the table `qa_test_cases`
func (s *Service) ImportQaTestCases(qaProjectId, qaProjectName string, file io.ReadCloser, incremental bool) errors.Error {
	if !incremental {
		// delete old data associated with this qaProjectId
		// delete qa_test_cases
		err := s.dal.Delete(&qa.QaTestCase{}, dal.Where("qa_project_id = ?", qaProjectId))
		if err != nil {
			return errors.Default.Wrap(err, fmt.Sprintf("failed to delete old qa_test_cases for qaProjectId %s", qaProjectId))
		}
		// delete qa_apis
		err = s.dal.Delete(&qa.QaApi{}, dal.Where("qa_project_id = ?", qaProjectId))
		if err != nil {
			return errors.Default.Wrap(err, fmt.Sprintf("failed to delete old qa_apis for qaProjectId %s", qaProjectId))
		}
		// delete qa_test_case_executions
		err = s.dal.Delete(&qa.QaTestCaseExecution{}, dal.Where("qa_project_id = ?", qaProjectId))
		if err != nil {
			return errors.Default.Wrap(err, fmt.Sprintf("failed to delete old qa_test_case_executions for qaProjectId %s", qaProjectId))
		}
		// never delete data in qa_projects
	}
	// create or update qa_projects
	err := s.dal.CreateOrUpdate(&qa.QaProject{
		DomainEntityExtended: domainlayer.DomainEntityExtended{
			Id: qaProjectId,
		},
		Name: qaProjectName,
	})
	if err != nil {
		return err
	}
	return s.importCSV(file, qaProjectId, s.qaTestCaseHandler(qaProjectId))
}

// qaTestCaseHandler saves a record into the `qa_test_cases` table
func (s *Service) qaTestCaseHandler(qaProjectId string) func(record map[string]interface{}) errors.Error {
	return func(record map[string]interface{}) errors.Error {
		creatorName, _ := getStringField(record, "creator_name", false)
		if creatorName != "" {
			creatorId, _ := s.createOrUpdateAccount(creatorName, qaProjectId)
			record["creator_id"] = creatorId
		}
		// remove fields
		delete(record, "creator_name")
		record["qa_project_id"] = qaProjectId
		return s.dal.CreateWithMap(&qa.QaTestCase{}, record)
	}
}

// ImportQaTestCaseExecutions imports csv file to the table `qa_test_case_executions`
func (s *Service) ImportQaTestCaseExecutions(qaProjectId string, file io.ReadCloser, incremental bool) errors.Error {
	if !incremental {
		// delete old data associated with this qaProjectId
		err := s.dal.Delete(&qa.QaTestCaseExecution{}, dal.Where("qa_project_id = ?", qaProjectId))
		if err != nil {
			return errors.Default.Wrap(err, fmt.Sprintf("failed to delete old qa_test_case_executions for qaProjectId %s", qaProjectId))
		}
	}
	return s.importCSV(file, qaProjectId, s.qaTestCaseExecutionHandler(qaProjectId))
}

// qaTestCaseExecutionHandler saves a record into the `qa_test_case_executions` table
func (s *Service) qaTestCaseExecutionHandler(qaProjectId string) func(record map[string]interface{}) errors.Error {
	// Assuming qa.QaTestCaseExecution model exists and CreateWithMap is suitable
	return func(record map[string]interface{}) errors.Error {
		creatorName, _ := getStringField(record, "creator_name", false)
		if creatorName != "" {
			creatorId, _ := s.createOrUpdateAccount(creatorName, qaProjectId)
			record["creator_id"] = creatorId
		}
		delete(record, "creator_name")
		record["qa_project_id"] = qaProjectId
		return s.dal.CreateWithMap(&qa.QaTestCaseExecution{}, record)
	}
}

// issueRepoCommitHandlerFactory returns a handler that will populate the `issue_commits` and `issue_repo_commits` table
// ths issueCommitsFields is used to filter the fields that should be inserted into the `issue_commits` table
func (s *Service) issueRepoCommitHandler(record map[string]interface{}) errors.Error {
	err := s.dal.CreateWithMap(&crossdomain.IssueRepoCommit{}, record)
	if err != nil {
		return err
	}
	// remove fields that not in table `issue_commits`
	delete(record, "host")
	delete(record, "namespace")
	delete(record, "repo_name")
	delete(record, "repo_url")
	return s.dal.CreateWithMap(&crossdomain.IssueCommit{}, record)
}

// ImportSprint imports csv file into the table `sprints`
func (s *Service) ImportSprint(boardId string, file io.ReadCloser, incremental bool) errors.Error {
	if !incremental {
		err := s.dal.Delete(
			&ticket.Sprint{},
			dal.Where("id IN (SELECT sprint_id FROM board_sprints WHERE board_id=? AND sprint_id NOT IN (SELECT sprint_id FROM board_sprints WHERE board_id!=?))", boardId, boardId),
		)
		if err != nil {
			return err
		}
		err = s.dal.Delete(
			&ticket.BoardSprint{},
			dal.Where("board_id = ?", boardId),
		)
		if err != nil {
			return err
		}
	}
	return s.importCSV(file, boardId, s.sprintHandler(boardId))
}

// sprintHandler saves a record into the `sprints` table
func (s *Service) sprintHandler(boardId string) func(record map[string]interface{}) errors.Error {
	return func(record map[string]interface{}) errors.Error {
		id, err := getStringField(record, "id", true)
		if err != nil {
			return err
		}
		record["original_board_id"] = boardId
		err = s.dal.CreateWithMap(&ticket.Sprint{}, record)
		if err != nil {
			return err
		}

		// Create board_sprint relation
		return s.dal.CreateOrUpdate(&ticket.BoardSprint{
			BoardId:  boardId,
			SprintId: id,
		})
	}
}

// ImportIssueChangelog imports csv file into the table `issue_changelogs`
func (s *Service) ImportIssueChangelog(boardId string, file io.ReadCloser, incremental bool) errors.Error {
	if !incremental {
		err := s.dal.Delete(
			&ticket.IssueChangelogs{},
			dal.Where("issue_id IN (SELECT issue_id FROM board_issues WHERE board_id=? AND issue_id NOT IN (SELECT issue_id FROM board_issues WHERE board_id!=?))", boardId, boardId),
		)
		if err != nil {
			return err
		}
	}
	return s.importCSV(file, boardId, s.issueChangelogHandler)
}

// issueChangelogHandler saves a record into the `issue_changelogs` table
func (s *Service) issueChangelogHandler(record map[string]interface{}) errors.Error {
	// create account
	authorName, err := getStringField(record, "author_name", false)
	if err != nil {
		return err
	}
	rawDataParams, err := getStringField(record, "_raw_data_params", true)
	if err != nil {
		return err
	}
	if authorName != "" {
		authorId, err := s.createOrUpdateAccount(authorName, rawDataParams)
		if err != nil {
			return err
		}
		record["author_id"] = authorId
	}
	// set field_id = field_name
	fieldName, err := getStringField(record, "field_name", true)
	if err != nil {
		return err
	}
	record["field_id"] = fieldName
	// handle assignee
	if fieldName == "assignee" {
		originalFromValue, err := getStringField(record, "original_from_value", false)
		if err != nil {
			return err
		}
		originalToValue, err := getStringField(record, "original_to_value", false)
		if err != nil {
			return err
		}
		fromId, err := s.createOrUpdateAccount(originalFromValue, rawDataParams)
		if err != nil {
			return err
		}
		record["original_from_value"] = fromId
		toId, err := s.createOrUpdateAccount(originalToValue, rawDataParams)
		if err != nil {
			return err
		}
		record["original_to_value"] = toId
	}
	return s.dal.CreateWithMap(&ticket.IssueChangelogs{}, record)
}

// ImportIssueWorklog imports csv file into the table `issue_worklogs`
func (s *Service) ImportIssueWorklog(boardId string, file io.ReadCloser, incremental bool) errors.Error {
	if !incremental {
		err := s.dal.Delete(
			&ticket.IssueWorklog{},
			dal.Where("issue_id IN (SELECT issue_id FROM board_issues WHERE board_id=? AND issue_id NOT IN (SELECT issue_id FROM board_issues WHERE board_id!=?))", boardId, boardId),
		)
		if err != nil {
			return err
		}
	}
	return s.importCSV(file, boardId, s.issueWorklogHandler)
}

// issueWorklogHandler saves a record into the `issue_worklogs` table
func (s *Service) issueWorklogHandler(record map[string]interface{}) errors.Error {
	// create account
	authorName, err := getStringField(record, "author_name", false)
	if err != nil {
		return err
	}
	if authorName != "" {
		rawDataParams, err := getStringField(record, "_raw_data_params", true)
		if err != nil {
			return err
		}
		authorId, err := s.createOrUpdateAccount(authorName, rawDataParams)
		if err != nil {
			return err
		}
		record["author_id"] = authorId
	}
	delete(record, "author_name")
	return s.dal.CreateWithMap(&ticket.IssueWorklog{}, record)
}
