package workitempipeline

import (
	"context"
	"encoding/json"
	"errors"
	"strings"

	"github.com/jackc/pgx/v5"
	"github.com/nomadcode/nomadcode-core/internal/projectsync"
	"github.com/nomadcode/nomadcode-core/internal/storage"
	"github.com/nomadcode/nomadcode-core/internal/workflow"
	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

// ErrProjectSyncNotConfigured is returned when the pipeline has no project sync
// binder wired. Plane-origin creation must resolve git/workspace bindings and
// reserve a workspace slot from project sync state, so an unconfigured binder
// fails explicitly instead of creating a task with an unbound workspace.
var ErrProjectSyncNotConfigured = errors.New("project sync resolver is not configured")

var ErrCreationTriggerIgnored = errors.New("creation trigger ignored")

type CreationTrigger struct {
	RequiredStateID    string
	RequiredAssigneeID string
	Actor              string
	SelfActor          string
}

type CreateTaskInput struct {
	Ref     workitem.Ref
	StateID string
	Comment string
	Trigger CreationTrigger
}

type TaskCreator interface {
	CreateTask(ctx context.Context, input workflow.CreateTaskInput) (storage.Task, error)
	GetTaskByExternalRef(ctx context.Context, provider, id string) (storage.Task, error)
	WithExternalRefLock(ctx context.Context, provider, id string, fn func(ctx context.Context) (storage.Task, error)) (storage.Task, error)
}

// ProjectBinding is the resolved project sync binding for a work item: the
// persisted setting ID plus its normalized config. The pipeline reserves a
// workspace slot against SettingID and reads git/workspace fields from Config.
type ProjectBinding struct {
	SettingID int64
	Config    projectsync.Config
}

// ProjectBinder resolves the active project sync binding for a work item's
// provider/project target and reserves an independent workspace slot for it.
// Implementations must key the binding strictly on the normalized
// provider/tenant/project so tickets from the same project share one config and
// never mix with another project's config, and must reserve one available slot
// atomically so concurrent requests never receive the same slot.
type ProjectBinder interface {
	ResolveProjectBinding(ctx context.Context, ref workitem.Ref) (ProjectBinding, error)
	EnsureProjectWorkspace(ctx context.Context, binding ProjectBinding) error
	ReserveWorkspaceSlot(ctx context.Context, projectSyncSettingID int64) (projectsync.WorkspaceSlot, error)
}

// CheckoutMetadata is the stable JSON contract recorded on a created task under
// the "checkout" key. git remote and source branch always come from the project
// sync config; slot id/index/path come from the reserved slot and the checkout
// plan; branch paths come from the provision plan so downstream sync layers can
// distinguish the develop scanning base from the slot's independent checkout.
type CheckoutMetadata struct {
	ProjectSyncSettingID int64  `json:"project_sync_setting_id"`
	WorkspaceID          string `json:"workspace_id"`
	GitRemoteURL         string `json:"git_remote_url"`
	SourceBranch         string `json:"source_branch"`
	ProjectWorkspaceRoot string `json:"project_workspace_root"`
	MainBranchPath       string `json:"main_branch_path"`
	DevelopBranchPath    string `json:"develop_branch_path"`
	SlotID               int64  `json:"slot_id"`
	SlotIndex            int    `json:"slot_index"`
	SlotPath             string `json:"slot_path"`
}

type Service struct {
	reader workitem.Reader
	binder ProjectBinder
	tasks  TaskCreator
}

func New(reader workitem.Reader, binder ProjectBinder, tasks TaskCreator) *Service {
	return &Service{reader: reader, binder: binder, tasks: tasks}
}

func (s *Service) CreateTaskFromWorkItem(ctx context.Context, input CreateTaskInput) (storage.Task, error) {
	if input.Ref.Provider != "" && input.Ref.ID != "" {
		return s.tasks.WithExternalRefLock(ctx, string(input.Ref.Provider), input.Ref.ID, func(ctx context.Context) (storage.Task, error) {
			existing, err := s.tasks.GetTaskByExternalRef(ctx, string(input.Ref.Provider), input.Ref.ID)
			if err == nil {
				return existing, nil
			}
			if !errors.Is(err, pgx.ErrNoRows) {
				return storage.Task{}, err
			}
			return s.createTaskFromWorkItemUnlocked(ctx, input)
		})
	}
	return s.createTaskFromWorkItemUnlocked(ctx, input)
}

func (s *Service) createTaskFromWorkItemUnlocked(ctx context.Context, input CreateTaskInput) (storage.Task, error) {
	if s.binder == nil {
		return storage.Task{}, ErrProjectSyncNotConfigured
	}
	binding, err := s.binder.ResolveProjectBinding(ctx, input.Ref)
	if err != nil {
		return storage.Task{}, err
	}
	config, err := binding.Config.Normalize()
	if err != nil {
		return storage.Task{}, err
	}

	// Fetch work item and evaluate creation trigger before creating workspace
	// side effects (EnsureProjectWorkspace / ReserveWorkspaceSlot).
	item, err := s.reader.FetchWorkItem(ctx, input.Ref)
	if err != nil {
		return storage.Task{}, err
	}
	if err := EvaluateCreationTrigger(item, input.Trigger); err != nil {
		return storage.Task{}, err
	}

	// Run workspace provision and slot reservation after passing trigger validation.
	if err := s.binder.EnsureProjectWorkspace(ctx, binding); err != nil {
		return storage.Task{}, err
	}
	slot, err := s.binder.ReserveWorkspaceSlot(ctx, binding.SettingID)
	if err != nil {
		return storage.Task{}, err
	}
	plan, err := projectsync.BuildCheckoutPlan(config, slot)
	if err != nil {
		return storage.Task{}, err
	}
	provision, err := projectsync.BuildProvisionPlan(config)
	if err != nil {
		return storage.Task{}, err
	}

	createInput, err := workitem.BuildCreateTaskInput(workitem.TaskCreateInput{
		Ref:     input.Ref,
		StateID: input.StateID,
		Comment: input.Comment,
	}, item)
	if err != nil {
		return storage.Task{}, err
	}
	createInput.Metadata, err = checkoutTaskMetadata(binding.SettingID, config, slot, plan, provision)
	if err != nil {
		return storage.Task{}, err
	}
	return s.tasks.CreateTask(ctx, createInput)
}

func checkoutTaskMetadata(settingID int64, config projectsync.Config, slot projectsync.WorkspaceSlot, plan projectsync.CheckoutPlan, provision projectsync.ProvisionPlan) (json.RawMessage, error) {
	return json.Marshal(map[string]any{
		"checkout": CheckoutMetadata{
			ProjectSyncSettingID: settingID,
			WorkspaceID:          config.WorkspaceID,
			GitRemoteURL:         plan.RemoteURL,
			SourceBranch:         plan.SourceBranch,
			ProjectWorkspaceRoot: plan.ProjectWorkspaceRoot,
			MainBranchPath:       provision.MainBranchPath,
			DevelopBranchPath:    provision.DevelopBranchPath,
			SlotID:               slot.ID,
			SlotIndex:            int(plan.SlotIndex),
			SlotPath:             plan.SlotPath,
		},
	})
}

func EvaluateCreationTrigger(item workitem.WorkItem, trigger CreationTrigger) error {
	actor := strings.TrimSpace(trigger.Actor)
	selfActor := strings.TrimSpace(trigger.SelfActor)
	if actor != "" && selfActor != "" && actor == selfActor {
		return ErrCreationTriggerIgnored
	}

	if trigger.RequiredStateID == "" && trigger.RequiredAssigneeID == "" {
		return nil
	}

	if trigger.RequiredStateID != "" {
		if strings.TrimSpace(item.State) != strings.TrimSpace(trigger.RequiredStateID) {
			return ErrCreationTriggerIgnored
		}
	}

	if trigger.RequiredAssigneeID != "" {
		if !item.HasAssignee(trigger.RequiredAssigneeID) {
			return ErrCreationTriggerIgnored
		}
	}

	return nil
}
