package integration

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"

	"github.com/warmbly/warmbly/internal/models"
)

// validateNativeActionConfig checks a native action node's config at write time
// (defense in depth — the dashboard validates too, but API callers may not).
func validateNativeActionConfig(action models.IntegrationAction, raw json.RawMessage) error {
	cfg := parseNativeConfig(raw)
	switch action {
	case models.IntegrationActionAddTag, models.IntegrationActionRemoveTag:
		if strings.TrimSpace(cfg.CategoryID) == "" {
			return fmt.Errorf("a tag action needs a tag")
		}
	case models.IntegrationActionCreateDeal, models.IntegrationActionMoveDealStage:
		if strings.TrimSpace(cfg.DealPipelineID) == "" || strings.TrimSpace(cfg.DealStageID) == "" {
			return fmt.Errorf("a deal action needs a pipeline and stage")
		}
	case models.IntegrationActionRunAutomation:
		if strings.TrimSpace(cfg.AutomationID) == "" {
			return fmt.Errorf("a run-automation action needs a target automation")
		}
	case models.IntegrationActionLabelEmail:
		if len(parseUUIDList(cfg.LabelIDs)) == 0 {
			return fmt.Errorf("a label action needs at least one label")
		}
	case models.IntegrationActionSetVariables:
		hasOne := false
		for _, v := range cfg.SetVars {
			if strings.TrimSpace(v.Key) != "" {
				hasOne = true
				break
			}
		}
		if !hasOne {
			return fmt.Errorf("set variables needs at least one named variable")
		}
	case models.IntegrationActionFireEvent:
		if strings.TrimSpace(cfg.EventName) == "" {
			return fmt.Errorf("a fire-event action needs an event name")
		}
	case models.IntegrationActionAIStep:
		return validateAIStepConfig(raw)
	case models.IntegrationActionAISwitch:
		return validateAISwitchConfig(raw)
	}
	return nil
}

// validateAISwitchConfig validates the AI switch router by decider mode
// (mirrors the campaign switch): "value" mode needs a value template and at
// least two cases (no model call, so no instruction); "ai" mode needs an
// instruction and at least two cases.
func validateAISwitchConfig(raw json.RawMessage) error {
	ai := parseAIConfig(raw)
	if len(nonEmptyStrings(ai.Cases)) < 2 {
		return fmt.Errorf("an AI switch needs at least two cases")
	}
	if strings.TrimSpace(ai.SwitchOn) == "value" {
		if strings.TrimSpace(ai.SwitchValue) == "" {
			return fmt.Errorf("a value switch needs a value to match")
		}
		return nil
	}
	if strings.TrimSpace(ai.Instruction) == "" {
		return fmt.Errorf("an AI switch needs an instruction")
	}
	return nil
}

// isAllowlistedAIAction is the closed set of REVERSIBLE native actions an
// agent-mode AI step may call as tools. Defined as its own switch (NOT derived
// from IsNativeAction) so it can never drift to include run_automation,
// fire_event, a connection-backed action, or any future send/reply action.
func isAllowlistedAIAction(a models.IntegrationAction) bool {
	switch a {
	case models.IntegrationActionAddTag,
		models.IntegrationActionRemoveTag,
		models.IntegrationActionCreateTask,
		models.IntegrationActionCreateDeal,
		models.IntegrationActionMoveDealStage,
		models.IntegrationActionLabelEmail,
		models.IntegrationActionSetVariables,
		models.IntegrationActionUnsubscribe:
		return true
	default:
		return false
	}
}

// validateAIStepConfig validates the unified AI step by mode. Routing lives in
// the AI switch node, so decide is not a step mode. Agent mode only checks the
// guarded allowlist: every entry must pass isAllowlistedAIAction, and the agent
// supplies each action's target (which tag/task/deal) as a tool argument at run
// time, so no pinned per-action config is required here.
func validateAIStepConfig(raw json.RawMessage) error {
	ai := parseAIConfig(raw)
	if strings.TrimSpace(ai.Instruction) == "" {
		return fmt.Errorf("an AI step needs an instruction")
	}
	switch strings.TrimSpace(ai.Mode) {
	case "classify":
		if len(nonEmptyStrings(ai.Labels)) < 2 {
			return fmt.Errorf("classify mode needs at least two labels")
		}
	case "extract":
		if len(nonEmptyStrings(ai.OutputKeys)) == 0 {
			return fmt.Errorf("extract mode needs at least one output key")
		}
	case "agent":
		enabled := nonEmptyStrings(ai.Allowlist)
		if len(enabled) == 0 {
			return fmt.Errorf("agent mode needs at least one allowed action")
		}
		for _, raw2 := range enabled {
			id := models.IntegrationAction(strings.TrimSpace(raw2))
			if !isAllowlistedAIAction(id) {
				return fmt.Errorf("%q is not an allowed agent action", raw2)
			}
		}
	case "generate", "":
		// generate needs only the instruction (already checked).
	default:
		return fmt.Errorf("unknown AI step mode %q", ai.Mode)
	}
	return nil
}

// parseUUIDList parses a slice of string ids into uuids, dropping any that don't
// parse. Used by the label_email action's category list.
func parseUUIDList(ids []string) []uuid.UUID {
	out := make([]uuid.UUID, 0, len(ids))
	for _, s := range ids {
		if id, err := uuid.Parse(strings.TrimSpace(s)); err == nil {
			out = append(out, id)
		}
	}
	return out
}

// NativeActions runs Warmbly-internal CRM/contact mutations for automation
// action nodes (no external connection). It's a consumer-side interface so the
// integration package needs no import of advanced/tasks/repository — a thin
// adapter in cmd/backend satisfies it (converting *errx.Error -> error and
// resolving the contact + org owner). Mirrors the AutomationRunner pattern.
type NativeActions interface {
	// ResolveContact finds the contact the action operates on, by id then email.
	ResolveContact(ctx context.Context, orgID uuid.UUID, contactID, email string) (*models.Contact, error)
	// OrgOwner returns the organization owner's user id (the actor for created
	// deals/tasks, which require a creator).
	OrgOwner(ctx context.Context, orgID uuid.UUID) (uuid.UUID, error)
	AddTag(ctx context.Context, orgID, actorID, contactID, categoryID uuid.UUID) error
	RemoveTag(ctx context.Context, orgID, actorID, contactID, categoryID uuid.UUID) error
	CreateTask(ctx context.Context, orgID, createdBy uuid.UUID, data *models.CreateCRMTask) error
	CreateDeal(ctx context.Context, orgID, createdBy uuid.UUID, data *models.CreateDeal) error
	MoveDealStage(ctx context.Context, orgID, contactID, pipelineID, stageID uuid.UUID) error
	Unsubscribe(ctx context.Context, campaignID, contactID uuid.UUID) error
	// LabelThread additively applies unibox conversation labels to a thread, on
	// behalf of the mailbox-owner userID (categories are per user). Backs the
	// "label_email" action; userID + threadID come from the reply event data.
	LabelThread(ctx context.Context, userID uuid.UUID, threadID string, categoryIDs []uuid.UUID) error

	// ListCategories / CreateCategory / ListPipelines back the AI agent step's
	// argument-based tools: the model picks a tag/label/pipeline by name and the
	// executor resolves it live (empty pool = any of the owner's tags). Keyed by
	// the org OWNER (categories are per user); pipelines are org-scoped with
	// stages hydrated in position order. Mirrors the campaign agent tools.
	ListCategories(ctx context.Context, ownerID uuid.UUID) ([]models.MiniCategory, error)
	CreateCategory(ctx context.Context, ownerID uuid.UUID, title, color string) (models.MiniCategory, error)
	ListPipelines(ctx context.Context, orgID uuid.UUID) ([]models.Pipeline, error)
}

// nativeActionConfig is the per-node config for native action nodes (mirrors the
// relevant subset of the campaign ActionConfig keys, stored in the node config).
type nativeActionConfig struct {
	CategoryID         string   `json:"category_id"`
	DealPipelineID     string   `json:"deal_pipeline_id"`
	DealStageID        string   `json:"deal_stage_id"`
	DealName           string   `json:"deal_name"`
	DealValue          *float64 `json:"deal_value"`
	DealCurrency       string   `json:"deal_currency"`
	TaskTitle          string   `json:"task_title"`
	TaskType           string   `json:"task_type"`
	TaskPriority       string   `json:"task_priority"`
	TaskDueOffsetDays  *int     `json:"task_due_offset_days"`
	TaskAssignedTo     string   `json:"task_assigned_to"`
	TaskAssignedTeamID string   `json:"task_assigned_team_id"`
	// run_automation: the automation to launch.
	AutomationID string `json:"automation_id"`
	// label_email: the unibox conversation labels to apply (category-registry ids).
	LabelIDs []string `json:"label_ids"`
	// set_variables: named values computed from templates and written back into
	// the event data for later nodes to reuse.
	SetVars []setVar `json:"set_vars"`
	// fire_event: a developer-defined custom event published to the realtime
	// gateway. EventName + each field value are Go-templated against the event data.
	EventName   string   `json:"event_name"`
	EventFields []setVar `json:"event_fields"`
}

type setVar struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

func parseNativeConfig(raw json.RawMessage) nativeActionConfig {
	var c nativeActionConfig
	if len(raw) > 0 {
		_ = json.Unmarshal(raw, &c)
	}
	return c
}

// execNativeAction resolves the contact from the event data and runs the native
// CRM/contact mutation. Returns an error (recorded in run history); never panics.
func (s *service) execNativeAction(ctx context.Context, a models.Automation, n models.AutomationNode, data map[string]any) error {
	if s.native == nil {
		return fmt.Errorf("native actions are not available")
	}
	cfg := parseNativeConfig(n.Config)

	// run_automation launches another automation against the same event data; it
	// does not need a resolved contact, so handle it before contact resolution.
	// The chain-depth guard in RunAutomationByID bounds recursion/compute.
	if n.Action == models.IntegrationActionRunAutomation {
		targetID, perr := uuid.Parse(strings.TrimSpace(cfg.AutomationID))
		if perr != nil {
			return fmt.Errorf("run-automation needs a target automation")
		}
		if targetID == a.ID {
			return fmt.Errorf("an automation cannot run itself")
		}
		return s.RunAutomationByID(ctx, a.OrganizationID, targetID, data)
	}

	// set_variables computes named values from templates and writes them back
	// into the event data for later nodes. No contact, no external call.
	if n.Action == models.IntegrationActionSetVariables {
		for _, v := range cfg.SetVars {
			key := strings.TrimSpace(v.Key)
			if key == "" {
				continue
			}
			data[key] = renderTemplate(v.Value, data)
		}
		return nil
	}

	// fire_event publishes a developer-defined custom event to the realtime
	// gateway (org-scoped). Subscribers receive it over the websocket with no
	// public URL. Name + each field value are templated against the event data.
	if n.Action == models.IntegrationActionFireEvent {
		name := strings.TrimSpace(renderTemplate(cfg.EventName, data))
		if name == "" {
			return fmt.Errorf("fire-event needs an event name")
		}
		payload := make(map[string]string, len(cfg.EventFields))
		for _, f := range cfg.EventFields {
			key := strings.TrimSpace(f.Key)
			if key == "" {
				continue
			}
			payload[key] = renderTemplate(f.Value, data)
		}
		if s.publisher != nil {
			s.publisher.PublishCustomEvent(ctx, a.OrganizationID, uuid.Nil, name, payload, "automation", a.ID.String())
		}
		return nil
	}

	// label_email tags the conversation the event belongs to; it needs the
	// thread + mailbox owner (carried by reply triggers), not a resolved contact.
	if n.Action == models.IntegrationActionLabelEmail {
		threadID := stringFromMap(data, "thread_id")
		ownerID, perr := uuid.Parse(stringFromMap(data, "_user_id"))
		if threadID == "" || perr != nil {
			return fmt.Errorf("label-email needs a reply thread (use it on a reply trigger)")
		}
		catIDs := parseUUIDList(cfg.LabelIDs)
		if len(catIDs) == 0 {
			return fmt.Errorf("a label action needs at least one label")
		}
		return s.native.LabelThread(ctx, ownerID, threadID, catIDs)
	}

	contactID := stringFromMap(data, "contact_id")
	email := stringFromMap(data, "contact_email", "invitee_email", "email")

	c, err := s.native.ResolveContact(ctx, a.OrganizationID, contactID, email)
	if err != nil || c == nil {
		return fmt.Errorf("no contact matched the event (need contact_id or contact_email)")
	}

	switch n.Action {
	case models.IntegrationActionUnsubscribe:
		campID, perr := uuid.Parse(stringFromMap(data, "campaign_id"))
		if perr != nil {
			return fmt.Errorf("unsubscribe needs a campaign_id in the event data")
		}
		return s.native.Unsubscribe(ctx, campID, c.ID)

	case models.IntegrationActionAddTag, models.IntegrationActionRemoveTag:
		catID, perr := uuid.Parse(cfg.CategoryID)
		if perr != nil {
			return fmt.Errorf("a tag action needs a tag")
		}
		owner, oerr := s.native.OrgOwner(ctx, a.OrganizationID)
		if oerr != nil {
			return oerr
		}
		if n.Action == models.IntegrationActionAddTag {
			return s.native.AddTag(ctx, a.OrganizationID, owner, c.ID, catID)
		}
		return s.native.RemoveTag(ctx, a.OrganizationID, owner, c.ID, catID)

	case models.IntegrationActionMoveDealStage:
		pid, e1 := uuid.Parse(cfg.DealPipelineID)
		sid, e2 := uuid.Parse(cfg.DealStageID)
		if e1 != nil || e2 != nil {
			return fmt.Errorf("move-deal-stage needs a pipeline and stage")
		}
		return s.native.MoveDealStage(ctx, a.OrganizationID, c.ID, pid, sid)

	case models.IntegrationActionCreateDeal:
		pid, e1 := uuid.Parse(cfg.DealPipelineID)
		sid, e2 := uuid.Parse(cfg.DealStageID)
		if e1 != nil || e2 != nil {
			return fmt.Errorf("create-deal needs a pipeline and stage")
		}
		owner, oerr := s.native.OrgOwner(ctx, a.OrganizationID)
		if oerr != nil {
			return oerr
		}
		name := renderTemplate(cfg.DealName, data)
		if name == "" {
			name = "Deal: " + c.Email
		}
		currency := cfg.DealCurrency
		if currency == "" {
			currency = "USD"
		}
		cid := c.ID
		return s.native.CreateDeal(ctx, a.OrganizationID, owner, &models.CreateDeal{
			PipelineID: pid,
			StageID:    sid,
			ContactID:  &cid,
			Name:       name,
			Value:      cfg.DealValue,
			Currency:   currency,
		})

	case models.IntegrationActionCreateTask:
		owner, oerr := s.native.OrgOwner(ctx, a.OrganizationID)
		if oerr != nil {
			return oerr
		}
		title := renderTemplate(cfg.TaskTitle, data)
		if title == "" {
			title = "Follow up: " + c.Email
		}
		cid := c.ID
		task := &models.CreateCRMTask{
			ContactID: &cid,
			Title:     title,
			Type:      cfg.TaskType,
			Priority:  cfg.TaskPriority,
		}
		// Assignee: a specific user, or a whole team, or (default) the org owner.
		if tid, perr := uuid.Parse(strings.TrimSpace(cfg.TaskAssignedTeamID)); perr == nil {
			task.AssignedTeamID = &tid
		}
		if uid, perr := uuid.Parse(strings.TrimSpace(cfg.TaskAssignedTo)); perr == nil {
			task.AssignedTo = &uid
		} else if task.AssignedTeamID == nil {
			task.AssignedTo = &owner
		}
		if cfg.TaskDueOffsetDays != nil {
			due := time.Now().UTC().AddDate(0, 0, *cfg.TaskDueOffsetDays)
			task.DueDate = &due
		}
		return s.native.CreateTask(ctx, a.OrganizationID, owner, task)
	}
	return fmt.Errorf("unknown native action: %s", n.Action)
}
