package gitlabcivalidator

import (
	"fmt"
	"gitflame.ru/CI_VLDR/errors"
	"strings"
	"time"
)

// computeEndPosition возвращает конечные координаты для фрагмента, предполагая, что текст находится на одной строке.
func computeEndPosition(startLine, startColumn int, text string) (endLine, endColumn int) {
	// Если текст пустой, выделим хотя бы один символ
	if len(text) == 0 {
		return startLine, startColumn + 1
	}
	return startLine, startColumn + len(text)
}

// ValidateGitLabCIFile — главная точка входа в библиотеку.
// Принимает на вход содержимое GitLab CI-файла (в формате YAML) и опции валидации.
// Возвращает ошибку, если есть проблемы в конфигурации (или nil, если всё хорошо).
func ValidateGitLabCIFile(content string, opts ValidationOptions) error {
	validationErr := &errors.ValidationError{}

	// 1) Проверяем, что содержимое не пустое
	if len(content) == 0 {
		validationErr.AddDetailWithSpan(errors.ErrEmptyFile, "Please provide content of .gitflame-ci.yml", 0, 0, 0, 0)
		return validationErr
	}

	// 2) Парсим YAML
	cfg, err := ParseGitLabCIConfig([]byte(content))
	if err != nil {
		validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
			fmt.Sprintf("ошибка синтаксического анализа YAML: %v", err),
			0, 0, 0, 0)
		return validationErr
	}

	// 3) (Опционально) Проверяем SHA проекта,
	//    если включена опция и "проект с заданным SHA существует, но SHA не содержится ни в одной ветке/теге"
	//if opts.VerifyProjectSHA && opts.ProjectSHA != "" {
	//	// Заглушка: предположим, что проект существует,
	//	// а также предположим, что SHA нигде не встречается
	//	// (в реальном мире тут нужно обращаться к GitLab API).
	//	shaExists := checkSHAExistsInBranchesOrTags(opts.ProjectSHA)
	//	if !shaExists {
	//		validationErr.Add(fmt.Errorf("указанный SHA (%s) не содержится ни в одной ветке или теге", opts.ProjectSHA))
	//	}
	//}

	// 4) Валидируем структуру конфигурации
	validateRootObject(cfg, validationErr)
	validateJobs(cfg, validationErr)
	validateChanges(cfg, validationErr)
	validatePaths(cfg, validationErr)
	validateService(cfg, validationErr)
	validateStages(cfg, validationErr)
	validateVariables(cfg, validationErr)
	validateIncludeRules(cfg, validationErr)

	if !validationErr.IsEmpty() {
		return validationErr
	}

	return nil
}

// checkSHAExistsInBranchesOrTags - фейковая функция для проверки SHA.
// В реальном проекте здесь вы бы обращались к GitLab API / репозиторию.
func checkSHAExistsInBranchesOrTags(sha string) bool {
	// Для демонстрации пусть всегда возвращает false,
	// т.е. "SHA не найден".
	return false
}

// validateRootObject проверяет допустимые ключи на корневом уровне.
func validateRootObject(cfg *GitLabCIConfig, ve *errors.ValidationError) {
	validateBeforeScript(cfg.BeforeScript, ve)
}

// validateBeforeScript проверяет, что поле before_script представлено строкой
// или вложенным массивом строк (до 10 уровней вложенности).
func validateBeforeScript(before interface{}, ve *errors.ValidationError) {
	if before == nil {
		return
	}
	// Если это строка — всё ок.
	if _, ok := before.(string); ok {
		return
	}
	// Если это массив, проверяем вложенность.
	if arr, ok := before.([]interface{}); ok {
		if !validateNestedStringArray(arr, 1, 10) {
			ve.AddDetailWithSpan(errors.ErrBeforeScriptInvalid,
				"before_script config should be a string or a nested array of strings up to 10 levels deep",
				0, 0, 0, 0)
		}
	} else {
		ve.AddDetailWithSpan(errors.ErrBeforeScriptInvalid,
			"before_script config should be a string or a nested array of strings up to 10 levels deep",
			0, 0, 0, 0)
	}
}

// validateNestedStringArray рекурсивно проверяет, что все элементы массива являются строками
// или вложенными массивами строк и что глубина вложенности не превышает maxLevel.
func validateNestedStringArray(arr []interface{}, currentLevel, maxLevel int) bool {
	if currentLevel > maxLevel {
		return false
	}
	for _, item := range arr {
		switch v := item.(type) {
		case string:
			// ок
		case []interface{}:
			if !validateNestedStringArray(v, currentLevel+1, maxLevel) {
				return false
			}
		default:
			return false
		}
	}
	return true
}

// validateService проверяет, что конфигурация сервиса имеет тип string или map[string]interface{}.
func validateService(cfg *GitLabCIConfig, ve *errors.ValidationError) {
	if cfg.Services == nil {
		return
	}
	switch cfg.Services.(type) {
	case string:
		// Корректно
	case map[string]interface{}:
		// Корректно
	default:
		ve.AddDetailWithSpan(errors.ErrServiceInvalid,
			"config should be a hash or a string",
			0, 0, 0, 0)
	}
}

// validateStages проверяет, что каждая стадия задана как непустая строка.
func validateStages(cfg *GitLabCIConfig, ve *errors.ValidationError) {
	for i, st := range cfg.Stages {
		if strings.TrimSpace(st) == "" {
			ve.AddDetailWithSpan(errors.ErrStageInvalid,
				fmt.Sprintf("stage at index %d is empty; stage config should be a string", i),
				0, 0, 0, 0)
		}
	}
}

// validateVariables проверяет корректность описания переменных.
// Если значение переменной — скаляр, оно должно быть строкой, числом или булевым значением.
// Если переменная задана как хэш, допустимы ключи: value, description, expand, options.
func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) {
	if cfg.Variables == nil {
		return
	}
	varsMap, ok := cfg.Variables.(map[string]interface{})
	if !ok {
		ve.AddDetailWithSpan(errors.ErrVariablesInvalid, "variables config should be a map", 0, 0, 0, 0)
		return
	}
	allowedKeys := map[string]struct{}{
		"value":       {},
		"description": {},
		"expand":      {},
		"options":     {},
	}
	for varName, val := range varsMap {
		if isScalar(val) {
			// всё ок
		} else if nested, ok := val.(map[string]interface{}); ok {
			invalidKeys := []string{}
			for k := range nested {
				if _, allowed := allowedKeys[k]; !allowed {
					invalidKeys = append(invalidKeys, k)
				}
			}
			if len(invalidKeys) > 0 {
				ve.AddDetailWithSpan(errors.ErrVariablesInvalidKey,
					fmt.Sprintf("variable '%s' uses invalid data keys: %s", varName, strings.Join(invalidKeys, ", ")),
					0, 0, 0, 0)
			}
		} else {
			ve.AddDetailWithSpan(errors.ErrVariablesInvalid,
				fmt.Sprintf("variable '%s' must be a scalar or a map", varName),
				0, 0, 0, 0)
		}
	}
}

// isScalar возвращает true, если значение является скаляром (string, bool, числовым типом).
func isScalar(val interface{}) bool {
	switch val.(type) {
	case string, bool, int, int64, float64, float32:
		return true
	default:
		return false
	}
}

// validateJobs проводит серию проверок для job'ов.
func validateJobs(cfg *GitLabCIConfig, ve *errors.ValidationError) {
	// Если jobs отсутствуют, возвращаем ошибку с позицией 0,0 (так как позиция не определена)
	if cfg.Jobs == nil || len(cfg.Jobs) == 0 {
		ve.AddDetailWithSpan(errors.ErrMissingJobs, "отсутствуют job'ы (jobs) в конфигурации", 0, 0, 0, 0)
		return
	}

	// Формируем список разрешённых стадий из cfg.Stages
	stagesAllowed := make(map[string]struct{})
	for _, st := range cfg.Stages {
		stagesAllowed[st] = struct{}{}
	}

	visibleCount := 0

	// Для проверки циклов зависимости можно собрать dependency graph (пока не используем для цикла)
	dependencyGraph := make(map[string][]string)

	// Обходим все job'ы
	for _, job := range cfg.Jobs {
		// a) Имя задачи не может быть пустым
		if job.Name == "" {
			ve.AddDetailWithSpan(errors.ErrJobNameBlank, "job name can't be blank", job.Line, job.Column, job.Line, job.Column)
			continue
		}

		// b) Если имя не начинается с точки — задача считается видимой
		if job.Name[0] != '.' {
			visibleCount++
		}

		//// c) Проверяем обязательное наличие script или run
		//scriptPresent := job.Script != nil
		//if !scriptPresent && job.Run == nil {
		//	ve.AddDetail(errors.ErrMissingScript,
		//		fmt.Sprintf("задача '%s' не содержит script или run", job.Name),
		//		job.Line, job.Column)
		//}

		// d) Нельзя использовать одновременно script и run
		if job.Script == nil && job.Run == nil && job.Trigger == nil {
			ve.AddDetail(errors.ErrMissingScript,
				fmt.Sprintf("задача '%s' не содержит script, run или trigger", job.Name),
				job.Line, job.Column)
		}

		// e) Если указан stage, он должен присутствовать в списке разрешённых
		if job.Stage != "" {
			if _, ok := stagesAllowed[job.Stage]; !ok {
				ve.AddDetail(errors.ErrJobStageNotExist,
					fmt.Sprintf("задача '%s': выбранный stage '%s' не существует; доступные stage: %v", job.Name, job.Stage, cfg.Stages),
					job.Line, job.Column)
			}
		}

		// f) Проверяем, что все зависимости (dependencies) ссылаются на существующие job'ы
		dependencyGraph[job.Name] = append(dependencyGraph[job.Name], job.Dependencies...)
		for _, dep := range job.Dependencies {
			depPos, _ := job.DependencyLines[dep]
			if _, ok := cfg.Jobs[dep]; !ok {
				ve.AddDetail(errors.ErrUndefinedDependency,
					fmt.Sprintf("задача '%s' (строка %d, символ %d) имеет зависимость '%s' (строка %d, символ %d), которой нет среди job'ов",
						job.Name, job.Line, job.Column, dep, depPos.Line, depPos.Column),
					job.Line, job.Column)
			} else {
				// Проверяем порядок stage — зависимость не должна идти позже
				if !checkStageOrder(cfg, dep, job.Name) {
					ve.AddDetail(errors.ErrInvalidStageOrder,
						fmt.Sprintf("задача '%s' имеет зависимость '%s' со стадией, идущей позже, чем '%s'", job.Name, dep, job.Name),
						job.Line, job.Column)
				}
			}
		}

		// g) Проверяем блок needs
		if len(job.Needs) > 0 {
			needNames := make(map[string]struct{})
			for _, nd := range job.Needs {
				// Не должно быть дублирующихся записей в needs
				if _, exist := needNames[nd]; exist {
					// Здесь, например, выделяем весь фрагмент с нужной зависимостью.
					// Предположим, что мы можем вычислить окончание как:
					endCol := job.Column + len(nd)
					ve.AddDetailWithSpan(errors.ErrDuplicateNeeds,
						fmt.Sprintf("задача '%s' содержит дублирующуюся зависимость '%s' в needs", job.Name, nd),
						job.Line, job.Column, job.Line, endCol)
				} else {
					needNames[nd] = struct{}{}
				}

				// Имя в needs не должно быть пустым
				if nd == "" {
					ve.AddDetailWithSpan(errors.ErrJobNameBlank,
						fmt.Sprintf("задача '%s' имеет needs с пустым именем задачи", job.Name),
						job.Line, job.Column, job.Line, job.Column)
				}

				// Проверяем максимальную длину имени
				if len(nd) > MaxJobNameLength {
					endCol := job.Column + len(nd)
					ve.AddDetailWithSpan(errors.ErrNeedNameTooLong,
						fmt.Sprintf("задача '%s' содержит needs '%s', длина имени задачи превышает %d", job.Name, nd, MaxJobNameLength),
						job.Line, job.Column, job.Line, endCol)
				}

				// Каждая задача, указанная в needs, должна существовать
				needPos, posExists := job.NeedLines[nd]
				if !posExists {
					needPos = struct{ Line, Column int }{Line: job.Line, Column: job.Column}
				}
				if _, ok := cfg.Jobs[nd]; !ok {
					endCol := needPos.Column + len(nd)
					ve.AddDetailWithSpan(errors.ErrUndefinedNeed,
						fmt.Sprintf("задача '%s' (Line %d, Col %d-%d) ссылается на несуществующую задачу '%s'", job.Name, needPos.Line, needPos.Column, endCol, nd),
						job.Line, job.Column,
						needPos.Line, endCol)
				} else if !checkStageOrder(cfg, nd, job.Name) {
					endCol := needPos.Column + len(nd)
					ve.AddDetailWithSpan(errors.ErrInvalidStageOrder,
						fmt.Sprintf("задача '%s' (Line %d, Col %d) имеет need '%s' (Line %d, Col %d-%d) со стадией, идущей позже, чем '%s'",
							job.Name, job.Line, job.Column, nd, needPos.Line, needPos.Column, endCol, job.Name),
						job.Line, job.Column,
						needPos.Line, endCol)
				}
				// Добавляем запись needs в граф зависимостей
				dependencyGraph[job.Name] = append(dependencyGraph[job.Name], nd)
			}
		}

		validateJobArtifacts(job, ve)
		validateJobDelayedParameters(job, ve)
		validateJobDependenciesNeedsConsistency(job, ve)
		validateJobRules(job, ve)

	}

	// Проверка: должна быть хотя бы одна видимая задача
	if visibleCount == 0 {
		ve.AddDetail(errors.ErrNoVisibleJob,
			"в разделе jobs не обнаружено ни одной «видимой» задачи", 0, 0)
	}

	// После обхода всех job проверяем граф зависимостей на циклы.
	cycle := detectCycles(dependencyGraph)
	if len(cycle) > 0 {
		ve.AddDetail(errors.ErrCyclicDependency,
			fmt.Sprintf("проверка циклических зависимостей: обнаружен цикл между job: %v", cycle),
			0, 0)
	}
}

func validateJobArtifacts(job *Job, ve *errors.ValidationError) {
	if job.Artifacts != nil {
		// Если paths не указаны или массив пустой, возвращаем ошибку
		if len(job.Artifacts.Paths) == 0 {
			ve.AddDetail(
				errors.ErrArtifactsPathsBlank,
				fmt.Sprintf("задача '%s': artifacts paths can't be blank", job.Name),
				job.Line, job.Column,
			)
		}
	}
}

// checkStageOrder проверяет, что stage у jobDep не позже, чем stage у jobName
func checkStageOrder(cfg *GitLabCIConfig, jobDep, jobName string) bool {
	depStage := cfg.Jobs[jobDep].Stage
	jobStage := cfg.Jobs[jobName].Stage

	// Если stage пустые, условимся, что порядок "любопытен",
	// но в реальном GitLab это тоже может вызывать ошибки.
	if depStage == "" || jobStage == "" {
		return true // условно пропустим
	}

	allStages := cfg.Stages

	depIndex := -1
	jobIndex := -1
	for i, st := range allStages {
		if st == depStage {
			depIndex = i
		}
		if st == jobStage {
			jobIndex = i
		}
	}

	// Если не нашли stage, тоже пропустим, ибо сам факт уже поднимался выше
	if depIndex == -1 || jobIndex == -1 {
		return true
	}

	// Зависимость не должна иметь stage с индексом больше, чем у job
	return depIndex <= jobIndex
}

func validateChanges(cfg *GitLabCIConfig, ve *errors.ValidationError) {
	if cfg.Changes == nil {
		return
	}
	switch v := cfg.Changes.(type) {
	case []interface{}:
		if len(v) > 50 {
			ve.AddDetail(errors.ErrChangesTooManyEntries, "has too many entries (maximum 50)", 0, 0)
		}
		for _, item := range v {
			if _, ok := item.(string); !ok {
				ve.AddDetail(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0)
				break
			}
		}
	case map[string]interface{}:
		paths, ok := v["paths"]
		if !ok {
			ve.AddDetail(errors.ErrChangesMissingPaths, "changes config hash must contain key 'paths'", 0, 0)
		} else {
			arr, ok := paths.([]interface{})
			if !ok {
				ve.AddDetail(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0)
			} else {
				if len(arr) > 50 {
					ve.AddDetail(errors.ErrChangesTooManyEntries, "has too many entries (maximum 50)", 0, 0)
				}
				for _, item := range arr {
					if _, ok := item.(string); !ok {
						ve.AddDetail(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0)
						break
					}
				}
			}
		}
	case string:
		ve.AddDetail(errors.ErrChangesInvalidType, "should be an array or a hash", 0, 0)
	default:
		ve.AddDetail(errors.ErrChangesInvalidType, "should be an array or a hash", 0, 0)
	}
}

func validatePaths(cfg *GitLabCIConfig, ve *errors.ValidationError) {
	if cfg.Paths == nil {
		return
	}
	arr, ok := cfg.Paths.([]interface{})
	if !ok {
		ve.AddDetail(errors.ErrPathsNotArrayOfStrings, "paths config should be an array of strings", 0, 0)
		return
	}
	for _, item := range arr {
		if _, ok := item.(string); !ok {
			ve.AddDetail(errors.ErrPathsNotArrayOfStrings, "paths config should be an array of strings", 0, 0)
			break
		}
	}
}

// validateJobDelayedParameters проверяет параметры для отложенных (delayed) задач.
func validateJobDelayedParameters(job *Job, ve *errors.ValidationError) {
	if job.When == "delayed" {
		if job.StartIn == "" {
			ve.AddDetail(errors.ErrStartInMissing,
				fmt.Sprintf("задача '%s': start_in should be specified for delayed job", job.Name),
				job.Line, job.Column)
		} else {
			d, err := time.ParseDuration(job.StartIn)
			if err != nil {
				ve.AddDetail(errors.ErrStartInInvalid,
					fmt.Sprintf("задача '%s': job start in should be a duration", job.Name),
					job.Line, job.Column)
			} else if d > 7*24*time.Hour {
				ve.AddDetail(errors.ErrStartInTooLong,
					fmt.Sprintf("задача '%s': job start in should not exceed the limit", job.Name),
					job.Line, job.Column)
			}
		}
	} else {
		// Если задача не отложенная, start_in должен быть пустым
		if job.StartIn != "" {
			ve.AddDetail(errors.ErrStartInMustBeBlank,
				fmt.Sprintf("задача '%s': job start in must be blank when not delayed", job.Name),
				job.Line, job.Column)
		}
	}
}

// validateJobDependenciesNeedsConsistency убеждается, что все зависимости присутствуют в needs.
func validateJobDependenciesNeedsConsistency(job *Job, ve *errors.ValidationError) {
	if len(job.Dependencies) > 0 && len(job.Needs) > 0 {
		needNames := make(map[string]struct{})
		for _, nd := range job.Needs {
			needNames[nd] = struct{}{}
		}
		for _, dep := range job.Dependencies {
			if _, ok := needNames[dep]; !ok {
				ve.AddDetail(errors.ErrDependencyNotInNeeds,
					fmt.Sprintf("задача '%s': dependency '%s' should be part of needs", job.Name, dep),
					job.Line, job.Column)
			}
		}
	}
}

func validateJobRules(job *Job, ve *errors.ValidationError) {
	if job.Rules != nil {
		// Если rules используется, не должно быть only или except
		if job.Only != nil && job.Except != nil {
			ve.AddDetail(errors.ErrRulesOnlyExcept,
				fmt.Sprintf("задача '%s': may not be used with rules: only, except", job.Name),
				job.Line, job.Column)
		} else if job.Only != nil {
			ve.AddDetail(errors.ErrRulesOnly,
				fmt.Sprintf("задача '%s': may not be used with rules: only", job.Name),
				job.Line, job.Column)
		} else if job.Except != nil {
			ve.AddDetail(errors.ErrRulesExcept,
				fmt.Sprintf("задача '%s': may not be used with rules: except", job.Name),
				job.Line, job.Column)
		}
		// Если в секции rules присутствует ключ if, проверим его тип и синтаксис
		if rulesMap, ok := job.Rules.(map[string]interface{}); ok {
			if ifVal, exists := rulesMap["if"]; exists {
				if ifStr, ok := ifVal.(string); ok {
					if !validateExpressionSyntax(ifStr) {
						ve.AddDetail(errors.ErrInvalidExpressionSyntax,
							fmt.Sprintf("задача '%s': invalid expression syntax", job.Name),
							job.Line, job.Column)
					}
				} else {
					ve.AddDetail(errors.ErrInvalidExpressionSyntax,
						fmt.Sprintf("задача '%s': 'if' in rules should be a string", job.Name),
						job.Line, job.Column)
				}
			}
			// Проверим, что ключ allow_failure, если присутствует, имеет булевое значение
			if af, exists := rulesMap["allow_failure"]; exists {
				if _, ok := af.(bool); !ok {
					ve.AddDetail(errors.ErrBooleanValue,
						fmt.Sprintf("задача '%s': allow_failure in rules should be a boolean value", job.Name),
						job.Line, job.Column)
				}
			}
			// Проверка допустимых ключей в rules
			allowed := map[string]struct{}{
				"if":            {},
				"when":          {},
				"start_in":      {},
				"allow_failure": {},
				"changes":       {},
				"exists":        {},
			}
			for key := range rulesMap {
				if _, ok := allowed[key]; !ok {
					ve.AddDetail(errors.ErrUnknownRulesKey,
						fmt.Sprintf("задача '%s': unknown key in rules: %s", job.Name, key),
						job.Line, job.Column)
				}
			}
		}
	}
}

// validateExpressionSyntax выполняет базовую проверку синтаксиса выражения.
// В реальной реализации здесь может быть полноценный парсер.
func validateExpressionSyntax(expr string) bool {
	// Для демонстрации считаем, что пустая строка или отсутствие каких-либо специальных символов — это ошибка.
	return expr != ""
}

// detectCycles принимает граф зависимостей в виде мапы: job -> список зависимых job,
// и возвращает список job, участвующих в цикле (если он обнаружен).
func detectCycles(dependencyGraph map[string][]string) []string {
	visited := make(map[string]bool)
	recStack := make(map[string]bool)
	var cycle []string

	// Рекурсивная функция для обхода графа.
	var dfs func(string) bool
	dfs = func(node string) bool {
		if recStack[node] {
			// Если node уже в рекурсивном стеке, значит обнаружен цикл.
			cycle = append(cycle, node)
			return true
		}
		if visited[node] {
			return false
		}
		visited[node] = true
		recStack[node] = true

		for _, neighbor := range dependencyGraph[node] {
			if dfs(neighbor) {
				cycle = append(cycle, node)
				return true
			}
		}

		recStack[node] = false
		return false
	}

	// Запускаем DFS для каждой job.
	for node := range dependencyGraph {
		if !visited[node] {
			if dfs(node) {
				break // если цикл обнаружен, прерываем обход
			}
		}
	}
	return cycle
}

// Функция для валидации include-правил
func validateIncludeRules(cfg *GitLabCIConfig, ve *errors.ValidationError) {
	if cfg.Include == nil {
		return
	}
	switch v := cfg.Include.(type) {
	case map[string]interface{}:
		validateIncludeRuleMap(v, ve)
	case []interface{}:
		for _, item := range v {
			if m, ok := item.(map[string]interface{}); ok {
				validateIncludeRuleMap(m, ve)
			} else {
				// Если элемент не является картой, можно считать его допустимым,
				// либо добавить дополнительную проверку при необходимости.
			}
		}
	default:
		// Если include задан не как карта и не как массив, пропускаем проверку.
	}
}

// Функция, которая проверяет отдельную карту include-правила
func validateIncludeRuleMap(m map[string]interface{}, ve *errors.ValidationError) {
	// Проверяем ключи "exists" и "changes"
	keysToCheck := []string{"exists", "changes"}
	for _, key := range keysToCheck {
		if val, exists := m[key]; exists {
			switch v := val.(type) {
			case string:
				// Корректное значение – строка.
			case []interface{}:
				// Проверяем, что все элементы массива – строки.
				for _, item := range v {
					if _, ok := item.(string); !ok {
						ve.AddDetail(errors.ErrIncludeRulesInvalid,
							fmt.Sprintf("include rule key '%s' should be an array of strings", key),
							0, 0)
						break
					}
				}
			default:
				ve.AddDetail(errors.ErrIncludeRulesInvalid,
					fmt.Sprintf("include rule key '%s' should be a string or an array of strings", key),
					0, 0)
			}
		}
	}
}