// Package handlers provides HTTP handlers for the rdev API.
package handlers

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/sanitize"
	"github.com/orchard9/rdev/internal/service"
	"github.com/orchard9/rdev/internal/validate"
	"github.com/orchard9/rdev/pkg/api"
)

// ClaudeRequest is the request body for POST /projects/{id}/claude.
type ClaudeRequest struct {
	Prompt       string   `json:"prompt"`
	StreamID     string   `json:"stream_id,omitempty"`
	SessionID    string   `json:"session_id,omitempty"`    // Resume a previous session
	Model        string   `json:"model,omitempty"`         // Model override (OpenCode only)
	AllowedTools []string `json:"allowed_tools,omitempty"` // Restrict tool access
}

// RunClaude executes a Claude command in the project's claudebox.
// POST /projects/{id}/claude
func (h *ProjectsHandler) RunClaude(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")

	var req ClaudeRequest
	if err := api.DecodeJSON(r, &req); err != nil {
		api.WriteBadRequest(w, r, "invalid request body")
		return
	}

	// Use new service if available
	if h.projectService != nil {
		result, err := h.projectService.ExecuteClaude(r.Context(), service.ExecuteClaudeRequest{
			ProjectID:    domain.ProjectID(id),
			Prompt:       req.Prompt,
			StreamID:     req.StreamID,
			SessionID:    req.SessionID,
			Model:        req.Model,
			AllowedTools: req.AllowedTools,
			Audit:        getAuditContext(r),
		})
		if err != nil {
			if errors.Is(err, domain.ErrProjectNotFound) {
				api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
				return
			}
			if errors.Is(err, domain.ErrInvalidCommand) || errors.Is(err, domain.ErrCommandSanitization) {
				api.WriteBadRequest(w, r, err.Error())
				return
			}
			api.WriteInternalError(w, r, "failed to execute command")
			return
		}
		api.WriteCreated(w, r, map[string]any{
			"id":         result.CommandID,
			"project":    id,
			"type":       "claude",
			"status":     "running",
			"stream_url": result.StreamURL,
		})
		return
	}

	// Legacy path using hexagonal types
	if h.projectRepo == nil || h.executor == nil {
		api.WriteInternalError(w, r, "no project service configured")
		return
	}

	project, err := h.projectRepo.Get(r.Context(), domain.ProjectID(id))
	if err != nil {
		if errors.Is(err, domain.ErrProjectNotFound) {
			api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
			return
		}
		api.WriteInternalError(w, r, "failed to get project")
		return
	}

	if err := validate.Required(req.Prompt, "prompt"); err != nil {
		api.WriteBadRequest(w, r, err.Error())
		return
	}

	// Sanitize prompt
	if err := sanitize.ClaudePrompt(req.Prompt); err != nil {
		api.WriteBadRequest(w, r, err.Error())
		return
	}

	// Validate stream ID
	if err := sanitize.StreamID(req.StreamID); err != nil {
		api.WriteBadRequest(w, r, err.Error())
		return
	}

	// Generate command ID
	cmdNum := h.cmdID.Add(1)
	cmdID := fmt.Sprintf("cmd-%s-%03d", id, cmdNum)
	if req.StreamID != "" {
		cmdID = req.StreamID
	}

	// Create the command using domain types
	cmd := &domain.Command{
		ID:        domain.CommandID(cmdID),
		ProjectID: domain.ProjectID(id),
		Type:      domain.CommandTypeClaude,
		Args:      []string{req.Prompt},
		StartedAt: time.Now(),
	}

	// Execute in background
	go h.executeCommand(r.Context(), cmd, project.PodName)

	api.WriteCreated(w, r, map[string]any{
		"id":         cmdID,
		"project":    id,
		"type":       "claude",
		"status":     "running",
		"stream_url": fmt.Sprintf("/projects/%s/events?stream_id=%s", id, cmdID),
	})
}

// ShellRequest is the request body for POST /projects/{id}/shell.
type ShellRequest struct {
	Command  string `json:"command"`
	StreamID string `json:"stream_id,omitempty"`
}

// RunShell executes a shell command in the project's claudebox.
// POST /projects/{id}/shell
func (h *ProjectsHandler) RunShell(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")

	var req ShellRequest
	if err := api.DecodeJSON(r, &req); err != nil {
		api.WriteBadRequest(w, r, "invalid request body")
		return
	}

	// Use new service if available
	if h.projectService != nil {
		result, err := h.projectService.ExecuteShell(r.Context(), service.ExecuteShellRequest{
			ProjectID: domain.ProjectID(id),
			Command:   req.Command,
			StreamID:  req.StreamID,
			Audit:     getAuditContext(r),
		})
		if err != nil {
			if errors.Is(err, domain.ErrProjectNotFound) {
				api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
				return
			}
			if errors.Is(err, domain.ErrInvalidCommand) || errors.Is(err, domain.ErrCommandSanitization) {
				api.WriteBadRequest(w, r, err.Error())
				return
			}
			api.WriteInternalError(w, r, "failed to execute command")
			return
		}
		api.WriteCreated(w, r, map[string]any{
			"id":         result.CommandID,
			"project":    id,
			"type":       "shell",
			"status":     "running",
			"stream_url": result.StreamURL,
		})
		return
	}

	// Legacy path using hexagonal types
	if h.projectRepo == nil || h.executor == nil {
		api.WriteInternalError(w, r, "no project service configured")
		return
	}

	project, err := h.projectRepo.Get(r.Context(), domain.ProjectID(id))
	if err != nil {
		if errors.Is(err, domain.ErrProjectNotFound) {
			api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
			return
		}
		api.WriteInternalError(w, r, "failed to get project")
		return
	}

	if err := validate.Required(req.Command, "command"); err != nil {
		api.WriteBadRequest(w, r, err.Error())
		return
	}

	// Sanitize command
	if err := sanitize.ShellCommand(req.Command); err != nil {
		api.WriteBadRequest(w, r, err.Error())
		return
	}

	// Validate stream ID
	if err := sanitize.StreamID(req.StreamID); err != nil {
		api.WriteBadRequest(w, r, err.Error())
		return
	}

	// Generate command ID
	cmdNum := h.cmdID.Add(1)
	cmdID := fmt.Sprintf("cmd-%s-%03d", id, cmdNum)
	if req.StreamID != "" {
		cmdID = req.StreamID
	}

	// Create the command using domain types
	cmd := &domain.Command{
		ID:        domain.CommandID(cmdID),
		ProjectID: domain.ProjectID(id),
		Type:      domain.CommandTypeShell,
		Args:      []string{req.Command},
		StartedAt: time.Now(),
	}

	// Execute in background
	go h.executeCommand(r.Context(), cmd, project.PodName)

	api.WriteCreated(w, r, map[string]any{
		"id":         cmdID,
		"project":    id,
		"type":       "shell",
		"status":     "running",
		"stream_url": fmt.Sprintf("/projects/%s/events?stream_id=%s", id, cmdID),
	})
}

// GitRequest is the request body for POST /projects/{id}/git.
type GitRequest struct {
	Args     []string `json:"args"`
	StreamID string   `json:"stream_id,omitempty"`
}

// RunGit executes a git command in the project's claudebox.
// POST /projects/{id}/git
func (h *ProjectsHandler) RunGit(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")

	var req GitRequest
	if err := api.DecodeJSON(r, &req); err != nil {
		api.WriteBadRequest(w, r, "invalid request body")
		return
	}

	// Use new service if available
	if h.projectService != nil {
		result, err := h.projectService.ExecuteGit(r.Context(), service.ExecuteGitRequest{
			ProjectID: domain.ProjectID(id),
			Args:      req.Args,
			StreamID:  req.StreamID,
			Audit:     getAuditContext(r),
		})
		if err != nil {
			if errors.Is(err, domain.ErrProjectNotFound) {
				api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
				return
			}
			if errors.Is(err, domain.ErrInvalidCommand) || errors.Is(err, domain.ErrCommandSanitization) {
				api.WriteBadRequest(w, r, err.Error())
				return
			}
			api.WriteInternalError(w, r, "failed to execute command")
			return
		}
		api.WriteCreated(w, r, map[string]any{
			"id":         result.CommandID,
			"project":    id,
			"type":       "git",
			"status":     "running",
			"stream_url": result.StreamURL,
		})
		return
	}

	// Legacy path using hexagonal types
	if h.projectRepo == nil || h.executor == nil {
		api.WriteInternalError(w, r, "no project service configured")
		return
	}

	project, err := h.projectRepo.Get(r.Context(), domain.ProjectID(id))
	if err != nil {
		if errors.Is(err, domain.ErrProjectNotFound) {
			api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", id))
			return
		}
		api.WriteInternalError(w, r, "failed to get project")
		return
	}

	if err := validate.RequiredSlice(req.Args, "args"); err != nil {
		api.WriteBadRequest(w, r, err.Error())
		return
	}

	// Sanitize git args
	if err := sanitize.GitArgs(req.Args); err != nil {
		api.WriteBadRequest(w, r, err.Error())
		return
	}

	// Validate stream ID
	if err := sanitize.StreamID(req.StreamID); err != nil {
		api.WriteBadRequest(w, r, err.Error())
		return
	}

	// Generate command ID
	cmdNum := h.cmdID.Add(1)
	cmdID := fmt.Sprintf("cmd-%s-%03d", id, cmdNum)
	if req.StreamID != "" {
		cmdID = req.StreamID
	}

	// Create the command using domain types
	cmd := &domain.Command{
		ID:        domain.CommandID(cmdID),
		ProjectID: domain.ProjectID(id),
		Type:      domain.CommandTypeGit,
		Args:      req.Args,
		StartedAt: time.Now(),
	}

	// Execute in background
	go h.executeCommand(r.Context(), cmd, project.PodName)

	api.WriteCreated(w, r, map[string]any{
		"id":         cmdID,
		"project":    id,
		"type":       "git",
		"status":     "running",
		"stream_url": fmt.Sprintf("/projects/%s/events?stream_id=%s", id, cmdID),
	})
}

// executeCommand runs a command and streams output to subscribers.
// Uses context.WithoutCancel to preserve tracing/values but allow independent timeout.
func (h *ProjectsHandler) executeCommand(parentCtx context.Context, cmd *domain.Command, podName string) {
	// Derive from parent to preserve tracing/values, but with independent cancellation
	ctx, cancel := context.WithTimeout(context.WithoutCancel(parentCtx), TimeoutLongRunning)
	defer cancel()

	cmdID := string(cmd.ID)
	result, _ := h.executor.Execute(ctx, cmd, podName, func(line domain.OutputLine) {
		h.streams.Send(cmdID, "output", map[string]any{
			"line":   line.Line,
			"stream": line.Stream,
		})
	})

	// Send completion event
	h.streams.Send(cmdID, "complete", map[string]any{
		"exit_code":   result.ExitCode,
		"duration_ms": result.DurationMs,
	})

	// Clean up stream after a delay
	go func() {
		time.Sleep(30 * time.Second)
		h.streams.Close(cmdID)
	}()
}
