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

import (
	"errors"
	"net/http"
	"strconv"

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

// maxRequestBodySize is the maximum allowed size for request bodies (1MB).
const maxRequestBodySize = 1 << 20

// BuildsHandler handles project-scoped build endpoints.
type BuildsHandler struct {
	buildService *service.BuildService
}

// NewBuildsHandler creates a new builds handler.
func NewBuildsHandler(buildService *service.BuildService) *BuildsHandler {
	return &BuildsHandler{
		buildService: buildService,
	}
}

// Mount registers the build routes.
func (h *BuildsHandler) Mount(r api.Router) {
	// Project-scoped build endpoints
	r.With(auth.RequireScope(auth.ScopeBuildWrite, auth.ScopeAdmin)).
		Post("/projects/{id}/builds", h.StartBuild)
	r.With(auth.RequireScope(auth.ScopeBuildRead, auth.ScopeAdmin)).
		Get("/projects/{id}/builds", h.ListBuilds)

	// Build detail by task ID
	r.With(auth.RequireScope(auth.ScopeBuildRead, auth.ScopeAdmin)).
		Get("/builds/{taskId}", h.GetBuild)
}

// StartBuildRequest is the request body for POST /projects/{id}/builds.
type StartBuildRequest struct {
	Prompt      string            `json:"prompt"`
	Template    string            `json:"template,omitempty"`
	Variables   map[string]string `json:"variables,omitempty"`
	AutoCommit  bool              `json:"auto_commit"`
	AutoPush    bool              `json:"auto_push"`
	CallbackURL string            `json:"callback_url,omitempty"`
	GitCloneURL string            `json:"git_clone_url,omitempty"` // Required when auto_commit or auto_push is true
}

// StartBuildResponse is the response for POST /projects/{id}/builds.
type StartBuildResponse struct {
	TaskID    string `json:"task_id"`
	ProjectID string `json:"project_id"`
	Status    string `json:"status"`
	StatusURL string `json:"status_url"`
	StreamURL string `json:"stream_url"` // SSE endpoint for real-time build events
}

// BuildAuditDTO is the data transfer object for build audit entries.
type BuildAuditDTO struct {
	TaskID      string          `json:"task_id"`
	ProjectID   string          `json:"project_id"`
	WorkerID    string          `json:"worker_id,omitempty"`
	Status      string          `json:"status"`
	Prompt      string          `json:"prompt"`
	Template    string          `json:"template,omitempty"`
	AutoCommit  bool            `json:"auto_commit"`
	AutoPush    bool            `json:"auto_push"`
	Result      *BuildResultDTO `json:"result,omitempty"`
	StartedAt   string          `json:"started_at"`
	CompletedAt string          `json:"completed_at,omitempty"`
	StreamURL   string          `json:"stream_url"` // SSE endpoint for real-time build events
}

// BuildResultDTO is the data transfer object for build results.
type BuildResultDTO struct {
	Success      bool              `json:"success"`
	Output       string            `json:"output,omitempty"`
	Error        string            `json:"error,omitempty"`
	ErrorCode    string            `json:"error_code,omitempty"` // Categorized error type for programmatic handling
	CommitSHA    string            `json:"commit_sha,omitempty"`
	FilesChanged []string          `json:"files_changed,omitempty"`
	DurationMs   int64             `json:"duration_ms"`
	Artifacts    map[string]string `json:"artifacts,omitempty"`
}

func toBuildAuditDTO(e *domain.BuildAuditEntry) *BuildAuditDTO {
	if e == nil {
		return nil
	}
	dto := &BuildAuditDTO{
		TaskID:     e.TaskID,
		ProjectID:  e.ProjectID,
		WorkerID:   e.WorkerID,
		Status:     string(e.Status),
		Prompt:     e.Spec.Prompt,
		Template:   e.Spec.Template,
		AutoCommit: e.Spec.AutoCommit,
		AutoPush:   e.Spec.AutoPush,
		StartedAt:  e.StartedAt.Format("2006-01-02T15:04:05Z07:00"),
		StreamURL:  "/projects/" + e.ProjectID + "/events?stream_id=" + e.TaskID,
	}
	if e.CompletedAt != nil {
		dto.CompletedAt = e.CompletedAt.Format("2006-01-02T15:04:05Z07:00")
	}
	if e.Result != nil {
		dto.Result = &BuildResultDTO{
			Success:      e.Result.Success,
			Output:       e.Result.Output,
			Error:        e.Result.Error,
			ErrorCode:    string(e.Result.ErrorCode),
			CommitSHA:    e.Result.CommitSHA,
			FilesChanged: e.Result.FilesChanged,
			DurationMs:   e.Result.DurationMs,
			Artifacts:    e.Result.Artifacts,
		}
	}
	return dto
}

// StartBuild enqueues a build task for a project.
// POST /projects/{id}/builds
func (h *BuildsHandler) StartBuild(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	if projectID == "" {
		api.WriteBadRequest(w, r, "project ID is required")
		return
	}

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

	if req.Prompt == "" {
		api.WriteBadRequest(w, r, "prompt is required")
		return
	}

	// Validate callback URL to prevent SSRF
	if req.CallbackURL != "" {
		if err := domain.ValidateCallbackURL(req.CallbackURL); err != nil {
			api.WriteBadRequest(w, r, err.Error())
			return
		}
	}

	spec := domain.BuildSpec{
		Prompt:      req.Prompt,
		Template:    req.Template,
		Variables:   req.Variables,
		AutoCommit:  req.AutoCommit,
		AutoPush:    req.AutoPush,
		CallbackURL: req.CallbackURL,
		GitCloneURL: req.GitCloneURL,
	}

	// Validate git_clone_url is provided when auto_commit or auto_push is enabled
	if (req.AutoCommit || req.AutoPush) && req.GitCloneURL == "" {
		api.WriteBadRequest(w, r, "git_clone_url is required when auto_commit or auto_push is enabled")
		return
	}

	taskID, err := h.buildService.StartBuild(r.Context(), projectID, spec)
	if err != nil {
		if errors.Is(err, domain.ErrPromptRequired) {
			api.WriteBadRequest(w, r, err.Error())
			return
		}
		api.WriteInternalError(w, r, "failed to start build")
		return
	}

	api.WriteCreated(w, r, StartBuildResponse{
		TaskID:    taskID,
		ProjectID: projectID,
		Status:    "pending",
		StatusURL: "/builds/" + taskID,
		StreamURL: "/projects/" + projectID + "/events?stream_id=" + taskID,
	})
}

// ListBuilds returns build history for a project.
// GET /projects/{id}/builds?limit=50
func (h *BuildsHandler) ListBuilds(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	if projectID == "" {
		api.WriteBadRequest(w, r, "project ID is required")
		return
	}

	limit := 50
	if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
		l, err := strconv.Atoi(limitStr)
		if err != nil {
			api.WriteBadRequest(w, r, "limit must be a valid integer")
			return
		}
		if l < 1 || l > 200 {
			api.WriteBadRequest(w, r, "limit must be between 1 and 200")
			return
		}
		limit = l
	}

	builds, err := h.buildService.ListBuilds(r.Context(), projectID, limit)
	if err != nil {
		api.WriteInternalError(w, r, "failed to list builds")
		return
	}

	dtos := make([]*BuildAuditDTO, len(builds))
	for i, b := range builds {
		dtos[i] = toBuildAuditDTO(b)
	}

	api.WriteSuccess(w, r, map[string]any{
		"builds":     dtos,
		"project_id": projectID,
		"total":      len(dtos),
	})
}

// GetBuild returns the status of a specific build.
// GET /builds/{taskId}
func (h *BuildsHandler) GetBuild(w http.ResponseWriter, r *http.Request) {
	taskID := chi.URLParam(r, "taskId")
	if taskID == "" {
		api.WriteBadRequest(w, r, "task ID is required")
		return
	}

	entry, err := h.buildService.GetBuildStatus(r.Context(), taskID)
	if err != nil {
		if errors.Is(err, domain.ErrBuildNotFound) {
			api.WriteNotFound(w, r, "build not found: "+taskID)
			return
		}
		api.WriteInternalError(w, r, "failed to get build status")
		return
	}

	api.WriteSuccess(w, r, toBuildAuditDTO(entry))
}
