package services

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"regexp"
	"strings"
	"time"

	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/models"
	"git.mchus.pro/mchus/quoteforge/internal/services/sync"
	"github.com/google/uuid"
	"gorm.io/gorm"
)

var (
	ErrProjectNotFound            = errors.New("project not found")
	ErrProjectForbidden           = errors.New("access to project forbidden")
	ErrProjectCodeExists          = errors.New("project code and variant already exist")
	ErrCannotDeleteMainVariant    = errors.New("cannot delete main variant")
	ErrReservedMainVariant        = errors.New("variant name 'main' is reserved")
	ErrCannotRenameMainVariant    = errors.New("cannot rename main variant")
	ErrProjectCodeInvalidChars    = errors.New("код опти содержит недопустимые символы (разрешены: буквы, цифры, дефис, точка, подчёркивание)")
	ErrProjectVariantInvalidChars = errors.New("имя варианта содержит недопустимые символы (разрешены: буквы, цифры, дефис, точка, подчёркивание)")
)

// projectCodeRe allows only URL-path-safe characters so project codes can appear directly in URLs.
var projectCodeRe = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)

type ProjectService struct {
	localDB *localdb.LocalDB
}

func NewProjectService(localDB *localdb.LocalDB) *ProjectService {
	return &ProjectService{localDB: localDB}
}

type CreateProjectRequest struct {
	Code       string  `json:"code"`
	Variant    string  `json:"variant,omitempty"`
	Name       *string `json:"name,omitempty"`
	TrackerURL string  `json:"tracker_url"`
}

type UpdateProjectRequest struct {
	Code       *string `json:"code,omitempty"`
	Variant    *string `json:"variant,omitempty"`
	Name       *string `json:"name,omitempty"`
	TrackerURL *string `json:"tracker_url,omitempty"`
}

type ProjectConfigurationsResult struct {
	ProjectUUID string                 `json:"project_uuid"`
	Configs     []models.Configuration `json:"configurations"`
	Total       float64                `json:"total"`
}

func (s *ProjectService) Create(ownerUsername string, req *CreateProjectRequest) (*models.Project, error) {
	var namePtr *string
	if req.Name != nil {
		name := strings.TrimSpace(*req.Name)
		if name != "" {
			namePtr = &name
		}
	}
	code := strings.TrimSpace(req.Code)
	if code == "" {
		return nil, fmt.Errorf("project code is required")
	}
	if !projectCodeRe.MatchString(code) {
		return nil, ErrProjectCodeInvalidChars
	}
	variant := strings.TrimSpace(req.Variant)
	if err := validateProjectVariantName(variant); err != nil {
		return nil, err
	}
	if err := s.ensureUniqueProjectCodeVariant("", code, variant); err != nil {
		return nil, err
	}

	now := time.Now()
	localProject := &localdb.LocalProject{
		UUID:          uuid.NewString(),
		OwnerUsername: ownerUsername,
		Code:          code,
		Variant:       variant,
		Name:          namePtr,
		TrackerURL:    normalizeProjectTrackerURL(code, req.TrackerURL),
		IsActive:      true,
		IsSystem:      false,
		CreatedAt:     now,
		UpdatedAt:     now,
		SyncStatus:    "pending",
	}
	if err := s.localDB.SaveProject(localProject); err != nil {
		return nil, err
	}
	if err := s.enqueueProjectPendingChange(localProject, "create"); err != nil {
		return nil, err
	}
	return localdb.LocalToProject(localProject), nil
}

func (s *ProjectService) Update(projectUUID, ownerUsername string, req *UpdateProjectRequest) (*models.Project, error) {
	localProject, err := s.localDB.GetProjectByUUID(projectUUID)
	if err != nil {
		return nil, ErrProjectNotFound
	}

	if req.Code != nil {
		code := strings.TrimSpace(*req.Code)
		if code == "" {
			return nil, fmt.Errorf("project code is required")
		}
		if !projectCodeRe.MatchString(code) {
			return nil, ErrProjectCodeInvalidChars
		}
		localProject.Code = code
	}
	if req.Variant != nil {
		newVariant := strings.TrimSpace(*req.Variant)
		// Block renaming of the main variant (empty Variant) — there must always be a main.
		if strings.TrimSpace(localProject.Variant) == "" && newVariant != "" {
			return nil, ErrCannotRenameMainVariant
		}
		localProject.Variant = newVariant
		if err := validateProjectVariantName(localProject.Variant); err != nil {
			return nil, err
		}
	}
	if err := s.ensureUniqueProjectCodeVariant(projectUUID, localProject.Code, localProject.Variant); err != nil {
		return nil, err
	}

	if req.Name != nil {
		name := strings.TrimSpace(*req.Name)
		if name == "" {
			localProject.Name = nil
		} else {
			localProject.Name = &name
		}
	}
	if req.TrackerURL != nil {
		localProject.TrackerURL = normalizeProjectTrackerURL(localProject.Code, *req.TrackerURL)
	} else if strings.TrimSpace(localProject.TrackerURL) == "" {
		localProject.TrackerURL = normalizeProjectTrackerURL(localProject.Code, "")
	}
	localProject.UpdatedAt = time.Now()
	localProject.SyncStatus = "pending"
	if err := s.localDB.SaveProject(localProject); err != nil {
		return nil, err
	}
	if err := s.enqueueProjectPendingChange(localProject, "update"); err != nil {
		return nil, err
	}
	return localdb.LocalToProject(localProject), nil
}

func (s *ProjectService) ensureUniqueProjectCodeVariant(excludeUUID, code, variant string) error {
	normalizedCode := normalizeProjectCode(code)
	normalizedVariant := normalizeProjectVariant(variant)
	if normalizedCode == "" {
		return fmt.Errorf("project code is required")
	}

	projects, err := s.localDB.GetAllProjects(true)
	if err != nil {
		return err
	}
	for i := range projects {
		project := projects[i]
		if excludeUUID != "" && project.UUID == excludeUUID {
			continue
		}
		if normalizeProjectCode(project.Code) == normalizedCode &&
			normalizeProjectVariant(project.Variant) == normalizedVariant {
			return ErrProjectCodeExists
		}
	}
	return nil
}

func normalizeProjectCode(code string) string {
	return strings.ToLower(strings.TrimSpace(code))
}

func normalizeProjectVariant(variant string) string {
	return strings.ToLower(strings.TrimSpace(variant))
}

func validateProjectVariantName(variant string) error {
	if normalizeProjectVariant(variant) == "main" {
		return ErrReservedMainVariant
	}
	if variant != "" && !projectCodeRe.MatchString(variant) {
		return ErrProjectVariantInvalidChars
	}
	return nil
}

func (s *ProjectService) Archive(projectUUID, ownerUsername string) error {
	return s.setProjectActive(projectUUID, ownerUsername, false)
}

func (s *ProjectService) Reactivate(projectUUID, ownerUsername string) error {
	return s.setProjectActive(projectUUID, ownerUsername, true)
}

func (s *ProjectService) DeleteVariant(projectUUID, ownerUsername string) error {
	localProject, err := s.localDB.GetProjectByUUID(projectUUID)
	if err != nil {
		return ErrProjectNotFound
	}
	if strings.TrimSpace(localProject.Variant) == "" {
		return ErrCannotDeleteMainVariant
	}
	return s.setProjectActive(projectUUID, ownerUsername, false)
}

func (s *ProjectService) setProjectActive(projectUUID, ownerUsername string, isActive bool) error {
	return s.localDB.DB().Transaction(func(tx *gorm.DB) error {
		var project localdb.LocalProject
		if err := tx.Where("uuid = ?", projectUUID).First(&project).Error; err != nil {
			return ErrProjectNotFound
		}
		if project.IsActive == isActive {
			return nil
		}

		project.IsActive = isActive
		project.UpdatedAt = time.Now()
		project.SyncStatus = "pending"
		if err := tx.Save(&project).Error; err != nil {
			return err
		}

		if err := s.enqueueProjectPendingChangeTx(tx, &project, boolToOp(isActive, "reactivate", "archive")); err != nil {
			return err
		}

		var configs []localdb.LocalConfiguration
		if err := tx.Where("project_uuid = ?", projectUUID).Find(&configs).Error; err != nil {
			return err
		}
		for i := range configs {
			cfg := configs[i]
			cfg.IsActive = isActive
			cfg.SyncStatus = "pending"
			cfg.UpdatedAt = time.Now()
			if err := tx.Save(&cfg).Error; err != nil {
				return err
			}

			modelCfg := localdb.LocalToConfiguration(&cfg)
			payload, err := json.Marshal(modelCfg)
			if err != nil {
				return err
			}
			change := &localdb.PendingChange{
				EntityType: "configuration",
				EntityUUID: cfg.UUID,
				Operation:  "update",
				Payload:    string(payload),
				CreatedAt:  time.Now(),
				Attempts:   0,
			}
			if err := tx.Create(change).Error; err != nil {
				return err
			}
		}

		return nil
	})
}

func (s *ProjectService) ListByUser(ownerUsername string, includeArchived bool) ([]models.Project, error) {
	localProjects, err := s.localDB.GetAllProjects(includeArchived)
	if err != nil {
		return nil, err
	}

	projects := make([]models.Project, 0, len(localProjects))
	for i := range localProjects {
		projects = append(projects, *localdb.LocalToProject(&localProjects[i]))
	}
	return projects, nil
}

func (s *ProjectService) GetByUUID(projectUUID, ownerUsername string) (*models.Project, error) {
	localProject, err := s.localDB.GetProjectByUUID(projectUUID)
	if err != nil {
		return nil, ErrProjectNotFound
	}
	return localdb.LocalToProject(localProject), nil
}

// GetByCode finds the main variant of a project by its code (case-insensitive).
func (s *ProjectService) GetByCode(code string) (*models.Project, error) {
	localProject, err := s.localDB.GetProjectByCode(code)
	if err != nil {
		return nil, ErrProjectNotFound
	}
	return localdb.LocalToProject(localProject), nil
}

// GetByCodeAndVariant finds a project by code + variant (both case-insensitive).
func (s *ProjectService) GetByCodeAndVariant(code, variant string) (*models.Project, error) {
	localProject, err := s.localDB.GetProjectByCodeAndVariant(code, variant)
	if err != nil {
		return nil, ErrProjectNotFound
	}
	return localdb.LocalToProject(localProject), nil
}

func (s *ProjectService) ListConfigurations(projectUUID, ownerUsername, status string) (*ProjectConfigurationsResult, error) {
	project, err := s.GetByUUID(projectUUID, ownerUsername)
	if err != nil {
		return nil, err
	}
	if !project.IsActive && status == "active" {
		return &ProjectConfigurationsResult{
			ProjectUUID: projectUUID,
			Configs:     []models.Configuration{},
			Total:       0,
		}, nil
	}

	query := s.localDB.DB().
		Preload("CurrentVersion").
		Where("project_uuid = ?", projectUUID).
		Order("CASE WHEN COALESCE(line_no, 0) <= 0 THEN 2147483647 ELSE line_no END ASC, created_at DESC, id DESC")

	switch status {
	case "active", "":
		query = query.Where("is_active = ?", true)
	case "archived":
		query = query.Where("is_active = ?", false)
	case "all":
	default:
		query = query.Where("is_active = ?", true)
	}

	var localConfigs []localdb.LocalConfiguration
	if err := query.Find(&localConfigs).Error; err != nil {
		return nil, err
	}

	configs := make([]models.Configuration, 0, len(localConfigs))
	total := 0.0
	for i := range localConfigs {
		localCfg := localConfigs[i]
		cfg := localdb.LocalToConfiguration(&localCfg)
		if cfg.TotalPrice != nil {
			total += *cfg.TotalPrice
		}
		configs = append(configs, *cfg)
	}

	return &ProjectConfigurationsResult{
		ProjectUUID: projectUUID,
		Configs:     configs,
		Total:       total,
	}, nil
}

func (s *ProjectService) ResolveProjectUUID(ownerUsername string, projectUUID *string) (*string, error) {
	if projectUUID == nil || strings.TrimSpace(*projectUUID) == "" {
		project, err := s.localDB.EnsureDefaultProject(ownerUsername)
		if err != nil {
			return nil, err
		}
		return &project.UUID, nil
	}

	project, err := s.localDB.GetProjectByUUID(strings.TrimSpace(*projectUUID))
	if err != nil {
		return nil, ErrProjectNotFound
	}
	if project.OwnerUsername != ownerUsername {
		return nil, ErrProjectForbidden
	}
	if !project.IsActive {
		return nil, fmt.Errorf("project is archived")
	}

	resolved := project.UUID
	return &resolved, nil
}

func normalizeProjectTrackerURL(projectCode, trackerURL string) string {
	trimmedURL := strings.TrimSpace(trackerURL)
	if trimmedURL != "" {
		return trimmedURL
	}

	trimmedCode := strings.TrimSpace(projectCode)
	if trimmedCode == "" {
		return ""
	}

	return "https://tracker.yandex.ru/" + url.PathEscape(trimmedCode)
}

func (s *ProjectService) enqueueProjectPendingChange(project *localdb.LocalProject, operation string) error {
	return s.enqueueProjectPendingChangeTx(s.localDB.DB(), project, operation)
}

func (s *ProjectService) enqueueProjectPendingChangeTx(tx *gorm.DB, project *localdb.LocalProject, operation string) error {
	payload := sync.ProjectChangePayload{
		EventID:        uuid.NewString(),
		ProjectUUID:    project.UUID,
		Operation:      operation,
		Snapshot:       *localdb.LocalToProject(project),
		CreatedAt:      time.Now().UTC(),
		IdempotencyKey: fmt.Sprintf("%s:%d:%s", project.UUID, project.UpdatedAt.UnixNano(), operation),
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	change := &localdb.PendingChange{
		EntityType: "project",
		EntityUUID: project.UUID,
		Operation:  operation,
		Payload:    string(raw),
		CreatedAt:  time.Now(),
		Attempts:   0,
	}
	return tx.Create(change).Error
}

func boolToOp(v bool, whenTrue, whenFalse string) string {
	if v {
		return whenTrue
	}
	return whenFalse
}
