package buildsvc

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	"qctextbuilder/internal/domain"
	"qctextbuilder/internal/mapping"
	"qctextbuilder/internal/qcclient"
	"qctextbuilder/internal/store"
	"qctextbuilder/internal/validation"
)

type StartBuildRequest struct {
	TemplateID  int64             `json:"templateId"`
	RequestName string            `json:"requestName"`
	GlobalData  map[string]any    `json:"globalData"`
	FieldValues map[string]string `json:"fieldValues"`
}

type BuildResult struct {
	BuildID string `json:"buildId"`
	QCJobID int64  `json:"qcJobId"`
	Status  string `json:"status"`
}

type Service interface {
	StartBuild(ctx context.Context, req StartBuildRequest) (*BuildResult, error)
	PollOnce(ctx context.Context, buildID string) error
	FetchEditorURL(ctx context.Context, buildID string) error
	GetBuild(ctx context.Context, buildID string) (*domain.SiteBuild, error)
}

type BuildService struct {
	qc            qcclient.Client
	templateStore store.TemplateStore
	manifestStore store.ManifestStore
	buildStore    store.BuildStore
	mapping       mapping.Service
	pollTimeout   time.Duration
	mu            sync.Mutex
	inFlightPolls map[string]struct{}
}

func New(qc qcclient.Client, templateStore store.TemplateStore, manifestStore store.ManifestStore, buildStore store.BuildStore, mappingSvc mapping.Service, pollTimeout time.Duration) *BuildService {
	if pollTimeout <= 0 {
		pollTimeout = 5 * time.Minute
	}
	return &BuildService{
		qc:            qc,
		templateStore: templateStore,
		manifestStore: manifestStore,
		buildStore:    buildStore,
		mapping:       mappingSvc,
		pollTimeout:   pollTimeout,
		inFlightPolls: make(map[string]struct{}),
	}
}

func (s *BuildService) StartBuild(ctx context.Context, req StartBuildRequest) (*BuildResult, error) {
	template, err := s.templateStore.GetTemplateByID(ctx, req.TemplateID)
	if err != nil {
		return nil, fmt.Errorf("get template: %w", err)
	}
	if !template.IsAITemplate {
		return nil, errors.New("only ai templates are allowed")
	}

	manifest, err := s.manifestStore.GetActiveManifestByTemplateID(ctx, req.TemplateID)
	if err != nil {
		return nil, fmt.Errorf("get active manifest: %w", err)
	}
	if !isBuildAllowed(template.ManifestStatus) {
		return nil, fmt.Errorf("template manifest status must be reviewed or validated, got %q", template.ManifestStatus)
	}

	filteredGlobalData := FilterGlobalData(req.GlobalData)
	if err := validation.ValidateBuildGlobalData(filteredGlobalData); err != nil {
		return nil, err
	}

	fields, err := s.manifestStore.ListFieldsByManifestID(ctx, manifest.ID)
	if err != nil {
		return nil, fmt.Errorf("list manifest fields: %w", err)
	}

	aiData, err := s.mapping.AssembleAIData(fields, req.FieldValues)
	if err != nil {
		return nil, fmt.Errorf("assemble aiData: %w", err)
	}
	if len(aiData) == 0 {
		return nil, errors.New("at least one enabled text field value is required")
	}

	qcReq := qcclient.CreateSiteRequest{
		TemplateID: req.TemplateID,
		GlobalData: filteredGlobalData,
		Content: qcclient.CreateSiteContent{
			AIData: aiData,
		},
	}

	requestName := strings.TrimSpace(req.RequestName)
	if requestName == "" {
		requestName = "build-" + strconv.FormatInt(time.Now().Unix(), 10)
	}
	buildID := strconv.FormatInt(time.Now().UnixNano(), 10)
	globalJSON, _ := json.Marshal(filteredGlobalData)
	aiDataJSON, _ := json.Marshal(aiData)
	finalPayload, _ := json.Marshal(qcReq)
	now := time.Now().UTC()

	build := domain.SiteBuild{
		ID:                buildID,
		TemplateID:        req.TemplateID,
		ManifestID:        manifest.ID,
		RequestName:       requestName,
		GlobalDataJSON:    globalJSON,
		AIDataJSON:        aiDataJSON,
		FinalSitesPayload: finalPayload,
		QCStatus:          "draft",
	}
	if err := s.buildStore.CreateBuild(ctx, build); err != nil {
		return nil, fmt.Errorf("create build: %w", err)
	}

	siteResp, siteRaw, err := s.qc.CreateSite(ctx, qcReq)
	if err != nil {
		_ = s.buildStore.UpdateBuildFromJob(ctx, buildID, "failed", nil, "", nil, wrapErrorRaw(err), &now)
		return nil, fmt.Errorf("post /sites: %w", err)
	}

	if err := s.buildStore.MarkBuildSubmitted(ctx, buildID, siteResp.JobID, normalizeQCStatus(siteResp.Status), siteRaw, now); err != nil {
		return nil, fmt.Errorf("save site submission: %w", err)
	}

	return &BuildResult{
		BuildID: buildID,
		QCJobID: siteResp.JobID,
		Status:  normalizeQCStatus(siteResp.Status),
	}, nil
}

func (s *BuildService) PollOnce(ctx context.Context, buildID string) error {
	if !s.acquirePollLease(buildID) {
		return nil
	}
	defer s.releasePollLease(buildID)

	build, err := s.buildStore.GetBuildByID(ctx, buildID)
	if err != nil {
		return fmt.Errorf("get build: %w", err)
	}
	if isTerminalStatus(build.QCStatus) {
		return nil
	}
	if build.QCJobID == nil {
		return errors.New("build has no qcJobId")
	}

	now := time.Now().UTC()
	if hasExceededTimeout(build, now, s.pollTimeout) {
		return s.buildStore.UpdateBuildFromJob(
			ctx,
			buildID,
			"timeout",
			build.QCSiteID,
			preservePreviewURL(build.QCPreviewURL, ""),
			nil,
			wrapErrorMessage("poll timeout exceeded"),
			&now,
		)
	}

	job, jobRaw, err := s.qc.GetJob(ctx, *build.QCJobID)
	now = time.Now().UTC()
	if err != nil {
		return s.buildStore.UpdateBuildFromJob(ctx, buildID, "failed", nil, "", nil, wrapErrorRaw(err), &now)
	}

	status := normalizeQCStatus(job.Status)
	if isTerminalStatus(build.QCStatus) {
		status = build.QCStatus
	}
	var finishedAt *time.Time
	var siteID *int64
	previewURL := strings.TrimSpace(job.Result.PreviewURL)
	if previewURL == "" {
		previewURL = build.QCPreviewURL
	}
	if job.Result.SiteID > 0 {
		id := job.Result.SiteID
		siteID = &id
	} else if build.QCSiteID != nil {
		siteID = build.QCSiteID
	}
	if isTerminalStatus(status) {
		finishedAt = &now
	}

	if err := s.buildStore.UpdateBuildFromJob(ctx, buildID, status, siteID, previewURL, jobRaw, nil, finishedAt); err != nil {
		return err
	}
	if status == "done" && siteID != nil && strings.TrimSpace(build.QCEditorURL) == "" {
		_ = s.fetchAndStoreEditorURL(ctx, buildID, *siteID)
	}
	return nil
}

func (s *BuildService) FetchEditorURL(ctx context.Context, buildID string) error {
	build, err := s.buildStore.GetBuildByID(ctx, buildID)
	if err != nil {
		return fmt.Errorf("get build: %w", err)
	}
	if !isTerminalStatus(build.QCStatus) {
		return fmt.Errorf("editor url can only be fetched for terminal build status, got %q", build.QCStatus)
	}
	if build.QCSiteID == nil {
		return errors.New("build has no qcSiteId")
	}
	if strings.TrimSpace(build.QCEditorURL) != "" {
		return nil
	}

	return s.fetchAndStoreEditorURL(ctx, buildID, *build.QCSiteID)
}

func (s *BuildService) GetBuild(ctx context.Context, buildID string) (*domain.SiteBuild, error) {
	return s.buildStore.GetBuildByID(ctx, buildID)
}

func isBuildAllowed(status string) bool {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "reviewed", "validated":
		return true
	default:
		return false
	}
}

func normalizeQCStatus(status string) string {
	s := strings.ToLower(strings.TrimSpace(status))
	switch s {
	case "", "unknown":
		return "queued"
	case "in_progress", "running":
		return "processing"
	case "success", "succeeded", "completed":
		return "done"
	case "error":
		return "failed"
	case "queued", "processing", "done", "failed", "timeout":
		return s
	default:
		return "processing"
	}
}

func wrapErrorRaw(err error) json.RawMessage {
	return wrapErrorMessage(err.Error())
}

func wrapErrorMessage(msg string) json.RawMessage {
	payload, marshalErr := json.Marshal(map[string]any{"error": msg})
	if marshalErr != nil {
		return nil
	}
	return payload
}

func preservePreviewURL(existing, next string) string {
	if strings.TrimSpace(next) != "" {
		return next
	}
	return existing
}

func isTerminalStatus(status string) bool {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "done", "failed", "timeout":
		return true
	default:
		return false
	}
}

func hasExceededTimeout(build *domain.SiteBuild, now time.Time, pollTimeout time.Duration) bool {
	if pollTimeout <= 0 || build.StartedAt == nil {
		return false
	}
	if isTerminalStatus(build.QCStatus) {
		return false
	}
	return now.Sub(*build.StartedAt) > pollTimeout
}

func (s *BuildService) fetchAndStoreEditorURL(ctx context.Context, buildID string, siteID int64) error {
	editor, raw, err := s.qc.GetEditorURL(ctx, siteID)
	if err != nil {
		return fmt.Errorf("get editor url: %w", err)
	}
	loginURL := strings.TrimSpace(editor.LoginURL)
	if loginURL == "" {
		return errors.New("empty editor login url")
	}
	return s.buildStore.UpdateBuildEditorURL(ctx, buildID, loginURL, raw)
}

func (s *BuildService) acquirePollLease(buildID string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.inFlightPolls[buildID]; ok {
		return false
	}
	s.inFlightPolls[buildID] = struct{}{}
	return true
}

func (s *BuildService) releasePollLease(buildID string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	delete(s.inFlightPolls, buildID)
}
