package handlers

import (
	"io"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"

	"chat-switchboard/auth"
	"chat-switchboard/middleware"
	"chat-switchboard/models"
	"chat-switchboard/store"
	"chat-switchboard/taskutil"
	"chat-switchboard/webhook"
)

// TaskHandler manages task CRUD and manual execution.
type TaskHandler struct {
	stores store.Stores
}

func NewTaskHandler(stores store.Stores) *TaskHandler {
	return &TaskHandler{stores: stores}
}

// ── List endpoints ──────────────────────────────

// ListMine returns tasks owned by the current user.
// GET /api/v1/tasks
func (h *TaskHandler) ListMine(c *gin.Context) {
	userID := c.GetString("user_id")
	tasks, err := h.stores.Tasks.ListByOwner(c.Request.Context(), userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list tasks"})
		return
	}
	if tasks == nil {
		tasks = []models.Task{}
	}
	c.JSON(http.StatusOK, gin.H{"data": tasks})
}

// ListAll returns all tasks (admin only).
// GET /api/v1/admin/tasks
func (h *TaskHandler) ListAll(c *gin.Context) {
	tasks, err := h.stores.Tasks.ListAll(c.Request.Context())
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list tasks"})
		return
	}
	if tasks == nil {
		tasks = []models.Task{}
	}
	c.JSON(http.StatusOK, gin.H{"data": tasks})
}

// ListTeamTasks returns tasks scoped to the given team.
// Any team member can view; RequireTeamMember middleware enforces access.
// GET /api/v1/teams/:teamId/tasks
func (h *TaskHandler) ListTeamTasks(c *gin.Context) {
	teamID := c.Param("teamId")
	tasks, err := h.stores.Tasks.ListByTeam(c.Request.Context(), teamID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list team tasks"})
		return
	}
	if tasks == nil {
		tasks = []models.Task{}
	}
	c.JSON(http.StatusOK, gin.H{"data": tasks})
}

// ── Create ──────────────────────────────────────

// CreateTeamTask creates a task scoped to the team.
// Team admins only (RequireTeamAdmin middleware).
// POST /api/v1/teams/:teamId/tasks
func (h *TaskHandler) CreateTeamTask(c *gin.Context) {
	teamID := c.Param("teamId")
	// Inject team context, then delegate to Create
	c.Set("force_team_id", teamID)
	c.Set("force_scope", "team")
	h.Create(c)
}

// Create creates a new task.
// POST /api/v1/tasks
func (h *TaskHandler) Create(c *gin.Context) {
	ctx := c.Request.Context()

	// v0.27.2: Check global task configuration
	taskCfg := taskutil.LoadTaskConfig(ctx, h.stores.GlobalConfig)
	if !taskCfg.Enabled {
		c.JSON(http.StatusForbidden, gin.H{"error": "tasks are disabled by the administrator"})
		return
	}

	var t models.Task
	if err := c.ShouldBindJSON(&t); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	t.OwnerID = c.GetString("user_id")

	// v0.27.5: Team task context override (set by CreateTeamTask)
	if forceTeam, ok := c.Get("force_team_id"); ok {
		teamID := forceTeam.(string)
		t.TeamID = &teamID
		t.Scope = "team"
	}
	if forceScope, ok := c.Get("force_scope"); ok {
		t.Scope = forceScope.(string)
	}

	// v0.27.2: Personal task check — non-admin users need tasks.allow_personal
	if t.Scope == "personal" || t.Scope == "" {
		if c.GetString("role") != "admin" && !taskCfg.AllowPersonal {
			c.JSON(http.StatusForbidden, gin.H{"error": "personal tasks are disabled — contact your administrator"})
			return
		}
	}

	// v0.28.0: Action tasks require task.action permission
	if t.TaskType == "action" {
		if c.GetString("role") != "admin" {
			perms := middleware.GetResolvedPermissions(c)
			if perms == nil || !perms[auth.PermTaskAction] {
				c.JSON(http.StatusForbidden, gin.H{"error": "permission required: " + auth.PermTaskAction})
				return
			}
		}
	}

	// v0.29.0: Starlark tasks — run sandboxed extension scripts.
	// Requires task.starlark permission. system_function holds package ID.
	if t.TaskType == "starlark" {
		if c.GetString("role") != "admin" {
			perms := middleware.GetResolvedPermissions(c)
			if perms == nil || !perms[auth.PermTaskStarlark] {
				c.JSON(http.StatusForbidden, gin.H{"error": "permission required: " + auth.PermTaskStarlark})
				return
			}
		}
		if t.SystemFunction == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "system_function (package_id) is required for starlark tasks"})
			return
		}
		// Verify package exists and is starlark tier
		pkg, err := h.stores.Packages.Get(c.Request.Context(), t.SystemFunction)
		if err != nil || pkg == nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "package not found: " + t.SystemFunction})
			return
		}
		if pkg.Tier != models.ExtTierStarlark {
			c.JSON(http.StatusBadRequest, gin.H{"error": "package " + t.SystemFunction + " is not a starlark package"})
			return
		}
	}

	// v0.28.0-audit: Workflow task execution is not yet implemented.
	// Reject at the API boundary to prevent silent wrong behavior
	// (workflow tasks would fall through to the prompt pipeline).
	if t.TaskType == "workflow" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "workflow task execution is not yet implemented — use task_type 'prompt' or 'action'"})
		return
	}

	// v0.28.6: System tasks — admin-only, requires valid system_function
	if t.TaskType == "system" {
		if c.GetString("role") != "admin" {
			c.JSON(http.StatusForbidden, gin.H{"error": "system tasks are admin-only"})
			return
		}
		if t.SystemFunction == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "system_function is required for system tasks"})
			return
		}
		if err := taskutil.ValidateSystemFunc(t.SystemFunction); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
	}

	// Validate required fields
	if t.Name == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
		return
	}
	if t.Schedule == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "schedule is required"})
		return
	}
	if t.TaskType == "prompt" && t.UserPrompt == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "user_prompt is required for prompt tasks"})
		return
	}

	// v0.28.0: Webhook schedule validation — cannot use cron for webhook-triggered tasks
	if t.Schedule == "webhook" {
		// Webhook tasks never have a cron schedule — they fire on inbound POST
	} else {
		// v0.27.2: Validate cron expression before persisting
		if err := taskutil.ValidateCron(t.Schedule); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid schedule: " + err.Error()})
			return
		}
	}

	// Defaults
	if t.Scope == "" {
		t.Scope = "personal"
	}
	if t.TaskType == "" {
		t.TaskType = "prompt"
	}
	if t.Timezone == "" {
		t.Timezone = "UTC"
	}
	if t.OutputMode == "" {
		t.OutputMode = "channel"
	}

	// v0.27.2: Apply global default budgets for zero-value fields
	taskCfg.ApplyDefaults(&t)

	// Compute initial next_run_at (webhook tasks have no schedule-based next_run)
	if t.Schedule == "webhook" {
		// No initial next_run_at — triggered externally
		t.NextRunAt = nil
	} else if t.Schedule == "once" {
		now := time.Now().UTC()
		t.NextRunAt = &now
	} else {
		// v0.27.2: Full cron parsing via robfig/cron/v3
		t.NextRunAt = taskutil.NextRunFromSchedule(t.Schedule, t.Timezone)
	}

	t.IsActive = true

	// v0.27.3: Generate webhook secret if webhook URL is provided
	if t.WebhookURL != "" && t.WebhookSecret == "" {
		t.WebhookSecret = webhook.GenerateSecret()
	}

	// v0.28.0: Generate trigger token for webhook-scheduled tasks
	if t.Schedule == "webhook" {
		t.TriggerToken = webhook.GenerateSecret()
	}

	if err := h.stores.Tasks.Create(ctx, &t); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create task: " + err.Error()})
		return
	}

	c.JSON(http.StatusCreated, t)
}

// ── Access control helpers ──────────────────────

// canAccessTask returns true if the user can view this task.
// Access: owner, system admin, or team member (for team-scoped tasks).
func (h *TaskHandler) canAccessTask(c *gin.Context, t *models.Task) bool {
	if c.GetString("role") == "admin" {
		return true
	}
	if t.OwnerID == c.GetString("user_id") {
		return true
	}
	// Team members can view team-scoped tasks
	if t.Scope == "team" && t.TeamID != nil {
		ok, _ := h.stores.Teams.IsMember(c.Request.Context(), *t.TeamID, c.GetString("user_id"))
		return ok
	}
	return false
}

// canMutateTask returns true if the user can edit/delete/run this task.
// Mutation: owner, system admin, or team admin (for team-scoped tasks).
func (h *TaskHandler) canMutateTask(c *gin.Context, t *models.Task) bool {
	if c.GetString("role") == "admin" {
		return true
	}
	if t.OwnerID == c.GetString("user_id") {
		return true
	}
	if t.Scope == "team" && t.TeamID != nil {
		ok, _ := h.stores.Teams.IsTeamAdmin(c.Request.Context(), *t.TeamID, c.GetString("user_id"))
		return ok
	}
	return false
}

// ── Get / Update / Delete ───────────────────────

// Get returns a single task.
// GET /api/v1/tasks/:id
func (h *TaskHandler) Get(c *gin.Context) {
	id := c.Param("id")
	t, err := h.stores.Tasks.GetByID(c.Request.Context(), id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	if !h.canAccessTask(c, t) {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	c.JSON(http.StatusOK, t)
}

// Update patches a task.
// PUT /api/v1/tasks/:id
func (h *TaskHandler) Update(c *gin.Context) {
	id := c.Param("id")
	ctx := c.Request.Context()

	// Ownership check
	t, err := h.stores.Tasks.GetByID(ctx, id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	if !h.canMutateTask(c, t) {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}

	var patch models.TaskPatch
	if err := c.ShouldBindJSON(&patch); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// v0.27.2: Validate new schedule if provided
	if patch.Schedule != nil && *patch.Schedule != "webhook" {
		if err := taskutil.ValidateCron(*patch.Schedule); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid schedule: " + err.Error()})
			return
		}
	}

	if err := h.stores.Tasks.Update(ctx, id, patch); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update task"})
		return
	}

	updated, _ := h.stores.Tasks.GetByID(ctx, id)

	// Recompute next_run_at if schedule or timezone changed
	if patch.Schedule != nil || patch.Timezone != nil {
		if updated.Schedule == "webhook" {
			// Webhook tasks have no cron-based next_run
			_ = h.stores.Tasks.SetNextRun(ctx, id, nil)
			updated.NextRunAt = nil
		} else {
			tz := updated.Timezone
			next := taskutil.NextRunFromSchedule(updated.Schedule, tz)
			_ = h.stores.Tasks.SetNextRun(ctx, id, next)
			updated.NextRunAt = next
		}
	}

	c.JSON(http.StatusOK, updated)
}

// Delete removes a task.
// DELETE /api/v1/tasks/:id
func (h *TaskHandler) Delete(c *gin.Context) {
	id := c.Param("id")
	ctx := c.Request.Context()

	t, err := h.stores.Tasks.GetByID(ctx, id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	if !h.canMutateTask(c, t) {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}

	if err := h.stores.Tasks.Delete(ctx, id); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete task"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"deleted": true, "id": id})
}

// ── Run History ─────────────────────────────────

// ListRuns returns run history for a task.
// GET /api/v1/tasks/:id/runs
func (h *TaskHandler) ListRuns(c *gin.Context) {
	id := c.Param("id")
	ctx := c.Request.Context()

	// Access check (v0.27.5)
	t, err := h.stores.Tasks.GetByID(ctx, id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	if !h.canAccessTask(c, t) {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}

	runs, err := h.stores.Tasks.ListRuns(ctx, id, 50)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list runs"})
		return
	}
	if runs == nil {
		runs = []models.TaskRun{}
	}
	c.JSON(http.StatusOK, gin.H{"data": runs})
}

// ── RunNow / KillRun ────────────────────────────

// RunNow triggers immediate execution of a task (sets next_run_at to now).
// POST /api/v1/tasks/:id/run
func (h *TaskHandler) RunNow(c *gin.Context) {
	id := c.Param("id")
	ctx := c.Request.Context()

	// v0.27.2: Check tasks enabled
	taskCfg := taskutil.LoadTaskConfig(ctx, h.stores.GlobalConfig)
	if !taskCfg.Enabled {
		c.JSON(http.StatusForbidden, gin.H{"error": "tasks are disabled by the administrator"})
		return
	}

	t, err := h.stores.Tasks.GetByID(ctx, id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	if !h.canMutateTask(c, t) {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}

	// Check for already-running execution
	active, _ := h.stores.Tasks.GetActiveRun(ctx, id)
	if active != nil {
		c.JSON(http.StatusConflict, gin.H{"error": "task already has an active run"})
		return
	}

	// Also check for queued runs
	queued, _ := h.stores.Tasks.GetQueuedRun(ctx, id)
	if queued != nil {
		c.JSON(http.StatusConflict, gin.H{"error": "task already has a queued run"})
		return
	}

	now := time.Now().UTC()
	if err := h.stores.Tasks.SetNextRun(ctx, id, now); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to schedule run"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"scheduled": true, "next_run_at": now})
}

// KillRun cancels the active run of a task.
// POST /api/v1/tasks/:id/kill
func (h *TaskHandler) KillRun(c *gin.Context) {
	id := c.Param("id")
	ctx := c.Request.Context()

	t, err := h.stores.Tasks.GetByID(ctx, id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}
	if !h.canMutateTask(c, t) {
		c.JSON(http.StatusNotFound, gin.H{"error": "task not found"})
		return
	}

	active, _ := h.stores.Tasks.GetActiveRun(ctx, id)
	if active == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "no active run to cancel"})
		return
	}

	if err := h.stores.Tasks.UpdateRun(ctx, active.ID, "cancelled", active.TokensUsed, active.ToolCalls, active.WallClock, "killed by user"); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to cancel run"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"killed": true, "run_id": active.ID})
}

// ════════════════════════════════════════════════
// Webhook Trigger Handler (v0.28.0)
// ════════════════════════════════════════════════
//
// POST /api/v1/hooks/t/:token — unauthenticated, token-based auth.
// External systems (CI, task chaining, etc.) POST here to fire a
// webhook-triggered task. The request body is stored as trigger_payload
// and forwarded to the executor.

// TriggerHandler handles inbound webhook triggers.
type TriggerHandler struct {
	stores store.Stores
}

func NewTriggerHandler(stores store.Stores) *TriggerHandler {
	return &TriggerHandler{stores: stores}
}

// Handle processes an inbound webhook trigger.
// POST /api/v1/hooks/t/:token
func (h *TriggerHandler) Handle(c *gin.Context) {
	token := c.Param("token")
	ctx := c.Request.Context()

	// Look up task by trigger token
	task, err := h.stores.Tasks.GetByTriggerToken(ctx, token)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return
	}

	// Validate task state
	if !task.IsActive {
		c.JSON(http.StatusGone, gin.H{"error": "task is inactive"})
		return
	}
	if task.Schedule != "webhook" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "task is not webhook-triggered"})
		return
	}

	// Check for already-active or queued run
	active, _ := h.stores.Tasks.GetActiveRun(ctx, task.ID)
	if active != nil {
		c.JSON(http.StatusConflict, gin.H{"error": "task already has an active run"})
		return
	}
	queued, _ := h.stores.Tasks.GetQueuedRun(ctx, task.ID)
	if queued != nil {
		c.JSON(http.StatusConflict, gin.H{"error": "task already has a queued run"})
		return
	}

	// Read trigger payload (optional)
	var triggerPayload string
	if c.Request.Body != nil {
		body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) // 1MB limit
		if err == nil && len(body) > 0 {
			triggerPayload = string(body)
		}
	}

	// Create a queued run with the trigger payload
	run := &models.TaskRun{
		TaskID:         task.ID,
		Status:         "queued",
		TriggerPayload: triggerPayload,
	}
	if err := h.stores.Tasks.CreateRun(ctx, run); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create run"})
		return
	}

	// Set next_run_at to now so the scheduler picks it up
	now := time.Now().UTC()
	if err := h.stores.Tasks.SetNextRun(ctx, task.ID, now); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to schedule run"})
		return
	}

	c.JSON(http.StatusAccepted, gin.H{
		"triggered": true,
		"run_id":    run.ID,
		"task_id":   task.ID,
	})
}

// ListSystemFunctions returns the available system function names and descriptions.
// GET /admin/system-functions
func (h *TaskHandler) ListSystemFunctions(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"data": taskutil.ListSystemFuncs()})
}
