package handlers

import (
	"encoding/json"
	"net/http"
	"strconv"
	"time"

	"github.com/go-chi/chi/v5"
	"xorm.io/xorm"

	"github.com/forgeo/forgebucket/internal/api/middleware"
	"github.com/forgeo/forgebucket/internal/events"
	"github.com/forgeo/forgebucket/internal/models"
)

type EnvironmentHandler struct {
	db  *xorm.Engine
	bus events.EventBus
}

func NewEnvironmentHandler(db *xorm.Engine, bus events.EventBus) *EnvironmentHandler {
	return &EnvironmentHandler{db: db, bus: bus}
}

// ── Environment CRUD ──────────────────────────────────────────────────────────

// ListEnvironments returns all environments for a repository, each annotated
// with its most recent deployment (or nil if none).
func (h *EnvironmentHandler) ListEnvironments(w http.ResponseWriter, r *http.Request) {
	repoID, ok := h.resolveRepo(w, r)
	if !ok {
		return
	}

	var envs []models.Environment
	if err := h.db.Where("repo_id = ?", repoID).Asc("name").Find(&envs); err != nil {
		jsonError(w, "could not list environments", http.StatusInternalServerError)
		return
	}
	if envs == nil {
		envs = []models.Environment{}
	}

	type envResponse struct {
		models.Environment
		LatestDeployment *models.Deployment `json:"latestDeployment"`
	}

	result := make([]envResponse, len(envs))
	for i, env := range envs {
		var latest models.Deployment
		found, _ := h.db.Where("env_id = ?", env.ID).Desc("id").Limit(1).Get(&latest)
		er := envResponse{Environment: env}
		if found {
			er.LatestDeployment = &latest
		}
		result[i] = er
	}

	jsonOK(w, result)
}

// CreateEnvironment creates a new named environment for a repository.
func (h *EnvironmentHandler) CreateEnvironment(w http.ResponseWriter, r *http.Request) {
	repoID, ok := h.resolveRepo(w, r)
	if !ok {
		return
	}

	var body struct {
		Name            string `json:"name"`
		URL             string `json:"url"`
		ProtectionRules string `json:"protectionRules"` // raw JSON string
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		jsonError(w, "invalid request body", http.StatusBadRequest)
		return
	}
	if body.Name == "" {
		jsonError(w, "name is required", http.StatusBadRequest)
		return
	}

	// Reject duplicate name within repo.
	var existing models.Environment
	if found, _ := h.db.Where("repo_id = ? AND name = ?", repoID, body.Name).Get(&existing); found {
		jsonError(w, "environment with this name already exists", http.StatusConflict)
		return
	}

	env := &models.Environment{
		RepoID:          repoID,
		Name:            body.Name,
		URL:             body.URL,
		ProtectionRules: body.ProtectionRules,
	}
	if _, err := h.db.Insert(env); err != nil {
		jsonError(w, "could not create environment", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(env) //nolint:errcheck
}

// GetEnvironment returns a single environment with its latest deployment.
func (h *EnvironmentHandler) GetEnvironment(w http.ResponseWriter, r *http.Request) {
	env, ok := h.resolveEnv(w, r)
	if !ok {
		return
	}

	var latest models.Deployment
	found, _ := h.db.Where("env_id = ?", env.ID).Desc("id").Limit(1).Get(&latest)

	type envResponse struct {
		models.Environment
		LatestDeployment *models.Deployment `json:"latestDeployment"`
	}
	resp := envResponse{Environment: *env}
	if found {
		resp.LatestDeployment = &latest
	}
	jsonOK(w, resp)
}

// UpdateEnvironment patches the URL and/or protection rules of an environment.
func (h *EnvironmentHandler) UpdateEnvironment(w http.ResponseWriter, r *http.Request) {
	env, ok := h.resolveEnv(w, r)
	if !ok {
		return
	}

	var body struct {
		URL             *string `json:"url"`
		ProtectionRules *string `json:"protectionRules"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		jsonError(w, "invalid request body", http.StatusBadRequest)
		return
	}

	cols := []string{}
	if body.URL != nil {
		env.URL = *body.URL
		cols = append(cols, "url")
	}
	if body.ProtectionRules != nil {
		env.ProtectionRules = *body.ProtectionRules
		cols = append(cols, "protection_rules")
	}
	if len(cols) > 0 {
		if _, err := h.db.ID(env.ID).Cols(cols...).Update(env); err != nil {
			jsonError(w, "could not update environment", http.StatusInternalServerError)
			return
		}
	}
	jsonOK(w, env)
}

// DeleteEnvironment removes an environment and all its deployment records.
func (h *EnvironmentHandler) DeleteEnvironment(w http.ResponseWriter, r *http.Request) {
	env, ok := h.resolveEnv(w, r)
	if !ok {
		return
	}

	// Cascade-delete deployments first (XORM has no cascade by default).
	h.db.Where("env_id = ?", env.ID).Delete(&models.Deployment{}) //nolint:errcheck
	if _, err := h.db.ID(env.ID).Delete(&models.Environment{}); err != nil {
		jsonError(w, "could not delete environment", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ── Deployment endpoints ──────────────────────────────────────────────────────

// ListDeployments returns all deployments for an environment, newest first.
func (h *EnvironmentHandler) ListDeployments(w http.ResponseWriter, r *http.Request) {
	env, ok := h.resolveEnv(w, r)
	if !ok {
		return
	}

	limit := 30
	if l, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && l > 0 && l <= 100 {
		limit = l
	}

	var deploys []models.Deployment
	if err := h.db.Where("env_id = ?", env.ID).Desc("id").Limit(limit).Find(&deploys); err != nil {
		jsonError(w, "could not list deployments", http.StatusInternalServerError)
		return
	}
	if deploys == nil {
		deploys = []models.Deployment{}
	}
	jsonOK(w, deploys)
}

// CreateDeployment records a new deployment event (pending → triggers the deploy workflow).
func (h *EnvironmentHandler) CreateDeployment(w http.ResponseWriter, r *http.Request) {
	env, ok := h.resolveEnv(w, r)
	if !ok {
		return
	}
	userID, _ := middleware.UserIDFromContext(r.Context())
	var actor models.User
	h.db.ID(userID).Cols("username").Get(&actor)

	var body struct {
		SHA         string `json:"sha"`
		Ref         string `json:"ref"`
		Description string `json:"description"`
		RunID       *int64 `json:"runId"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		jsonError(w, "invalid request body", http.StatusBadRequest)
		return
	}
	if body.SHA == "" {
		jsonError(w, "sha is required", http.StatusBadRequest)
		return
	}

	now := time.Now().UTC()
	deploy := &models.Deployment{
		EnvID:       env.ID,
		RepoID:      env.RepoID,
		SHA:         body.SHA,
		Ref:         body.Ref,
		Status:      models.DeployStatusPending,
		TriggeredBy: actor.Username,
		Description: body.Description,
		RunID:       body.RunID,
		StartedAt:   &now,
	}
	if _, err := h.db.Insert(deploy); err != nil {
		jsonError(w, "could not create deployment", http.StatusInternalServerError)
		return
	}

	h.publishDeployEvent("deployment.started", env, deploy)

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(deploy) //nolint:errcheck
}

// UpdateDeploymentStatus allows external systems (CI runners, webhooks) to
// advance a deployment through its lifecycle states.
func (h *EnvironmentHandler) UpdateDeploymentStatus(w http.ResponseWriter, r *http.Request) {
	env, ok := h.resolveEnv(w, r)
	if !ok {
		return
	}
	deployID, err := strconv.ParseInt(chi.URLParam(r, "deployID"), 10, 64)
	if err != nil {
		jsonError(w, "invalid deployment ID", http.StatusBadRequest)
		return
	}

	var deploy models.Deployment
	if found, _ := h.db.Where("id = ? AND env_id = ?", deployID, env.ID).Get(&deploy); !found {
		jsonError(w, "deployment not found", http.StatusNotFound)
		return
	}

	var body struct {
		Status      string `json:"status"`
		Description string `json:"description"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		jsonError(w, "invalid request body", http.StatusBadRequest)
		return
	}

	allowed := map[string]bool{
		"in_progress": true,
		"success":     true,
		"failure":     true,
		"cancelled":   true,
	}
	if !allowed[body.Status] {
		jsonError(w, "invalid status; must be in_progress, success, failure, or cancelled", http.StatusBadRequest)
		return
	}

	deploy.Status = models.DeployStatus(body.Status)
	if body.Description != "" {
		deploy.Description = body.Description
	}

	cols := []string{"status", "description"}
	if deploy.Status == models.DeployStatusSuccess ||
		deploy.Status == models.DeployStatusFailure ||
		deploy.Status == models.DeployStatusCancelled {
		now := time.Now().UTC()
		deploy.FinishedAt = &now
		cols = append(cols, "finished_at")
	}

	if _, err := h.db.ID(deploy.ID).Cols(cols...).Update(&deploy); err != nil {
		jsonError(w, "could not update deployment status", http.StatusInternalServerError)
		return
	}

	subject := map[string]string{
		"success":   "deployment.succeeded",
		"failure":   "deployment.failed",
		"cancelled": "deployment.failed",
	}[body.Status]
	if subject != "" {
		h.publishDeployEvent(subject, env, &deploy)
	}

	jsonOK(w, deploy)
}

// ── Helpers ───────────────────────────────────────────────────────────────────

func (h *EnvironmentHandler) resolveRepo(w http.ResponseWriter, r *http.Request) (int64, bool) {
	owner := chi.URLParam(r, "owner")
	repoName := chi.URLParam(r, "repo")
	var u models.User
	if found, _ := h.db.Where("username = ?", owner).Get(&u); !found {
		jsonError(w, "repository not found", http.StatusNotFound)
		return 0, false
	}
	var repo models.Repository
	if found, _ := h.db.Where("owner_id = ? AND name = ?", u.ID, repoName).Get(&repo); !found {
		jsonError(w, "repository not found", http.StatusNotFound)
		return 0, false
	}
	return repo.ID, true
}

func (h *EnvironmentHandler) resolveEnv(w http.ResponseWriter, r *http.Request) (*models.Environment, bool) {
	repoID, ok := h.resolveRepo(w, r)
	if !ok {
		return nil, false
	}
	envName := chi.URLParam(r, "envName")
	var env models.Environment
	if found, _ := h.db.Where("repo_id = ? AND name = ?", repoID, envName).Get(&env); !found {
		jsonError(w, "environment not found", http.StatusNotFound)
		return nil, false
	}
	return &env, true
}

type deployEventPayload struct {
	DeploymentID int64                `json:"deploymentId"`
	EnvID        int64                `json:"envId"`
	EnvName      string               `json:"envName"`
	RepoID       int64                `json:"repoId"`
	SHA          string               `json:"sha"`
	Ref          string               `json:"ref"`
	Status       models.DeployStatus  `json:"status"`
	TriggeredBy  string               `json:"triggeredBy"`
}

func (h *EnvironmentHandler) publishDeployEvent(subject string, env *models.Environment, d *models.Deployment) {
	h.bus.Publish(subject, deployEventPayload{ //nolint:errcheck
		DeploymentID: d.ID,
		EnvID:        env.ID,
		EnvName:      env.Name,
		RepoID:       d.RepoID,
		SHA:          d.SHA,
		Ref:          d.Ref,
		Status:       d.Status,
		TriggeredBy:  d.TriggeredBy,
	})
}
