package httpx

import (
	"net/http"
	"strconv"
	"strings"
	"time"

	"go.mongodb.org/mongo-driver/v2/bson"

	"bountyboard/internal/chat"
	"bountyboard/internal/domain"
	"bountyboard/internal/files"
	"bountyboard/internal/store"
	"bountyboard/internal/workperform"
)

func (s *Server) routesConsultant(mux *http.ServeMux) {
	con := func(h http.HandlerFunc) http.Handler { return s.authedRole("consultant", h) }
	mux.Handle("POST /api/v1/tasks/{id}/approve-claim", con(s.handleApproveClaim))
	mux.Handle("POST /api/v1/tasks/{id}/decline-claim", con(s.handleDeclineClaim))
	mux.Handle("POST /api/v1/tasks/{id}/unassign", con(s.handleUnassign))
	mux.Handle("POST /api/v1/tasks/{id}/assign-ai", con(s.handleAssignAI))
	mux.Handle("POST /api/v1/tasks/{id}/review", con(s.handleReview))
	mux.Handle("GET /api/v1/consultant/reviews", con(s.handleReviewQueue))
	mux.Handle("GET /api/v1/consultant/pool", con(s.handleListPool))
	mux.Handle("POST /api/v1/consultant/pool", con(s.handleAddPool))
	mux.Handle("DELETE /api/v1/consultant/pool/{developerId}", con(s.handleRemovePool))
}

func (s *Server) handleApproveClaim(w http.ResponseWriter, r *http.Request) {
	var req struct {
		DeveloperID string `json:"developerId"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	t, _, ok := s.consultantTask(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	if !t.HasClaimFrom(req.DeveloperID) {
		writeError(w, http.StatusBadRequest, "no_claim", "that developer has not requested this task")
		return
	}
	me := CurrentUser(r.Context())
	declined := []string{}
	for _, cr := range t.ClaimRequests {
		if cr.DeveloperID != req.DeveloperID {
			declined = append(declined, cr.DeveloperID)
		}
	}
	err := s.store.TransitionTask(r.Context(), t, domain.StatusAssigned, me.ID, "claim_approved",
		map[string]any{"developerId": req.DeveloperID},
		map[string]any{
			"assignee":      domain.Assignee{Kind: "human", UserID: req.DeveloperID},
			"claimRequests": []domain.ClaimRequest{},
		})
	if err != nil {
		s.taskTransitionError(w, err)
		return
	}
	s.notifyUser(r.Context(), req.DeveloperID, "claim_approved",
		"You got the task: "+t.Title, "Bounty "+formatBounty(t.Bounty), "/tasks/"+t.ID)
	for _, id := range declined { // §4.4: declined devs are notified
		s.notifyUser(r.Context(), id, "claim_declined",
			"Task went to someone else: "+t.Title, "", "/board")
	}
	s.Publish("board", "task.assigned", map[string]any{"taskId": t.ID, "customerId": t.CustomerID})
	w.WriteHeader(http.StatusNoContent)
}

func (s *Server) handleDeclineClaim(w http.ResponseWriter, r *http.Request) {
	var req struct {
		DeveloperID string `json:"developerId"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	t, _, ok := s.consultantTask(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	if !t.HasClaimFrom(req.DeveloperID) {
		writeError(w, http.StatusBadRequest, "no_claim", "that developer has not requested this task")
		return
	}
	s.notifyUser(r.Context(), req.DeveloperID, "claim_declined",
		"Assignment request declined: "+t.Title, "", "/board")
	s.removeClaim(w, r, t, req.DeveloperID, CurrentUser(r.Context()).ID, "claim_declined")
}

func (s *Server) handleUnassign(w http.ResponseWriter, r *http.Request) {
	t, _, ok := s.consultantTask(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	prev := t.Assignee
	// best-effort cancel for AI jobs
	if prev != nil && prev.Kind == "ai" && prev.JobID != "" && s.performerClient != nil {
		if err := s.performerClient.Cancel(r.Context(), prev.JobID); err != nil {
			s.log.Warn("cancel wp job", "jobId", prev.JobID, "err", err)
		}
	}
	err := s.store.TransitionTask(r.Context(), t, domain.StatusPublished,
		CurrentUser(r.Context()).ID, "unassigned", nil, map[string]any{"assignee": nil})
	if err != nil {
		s.taskTransitionError(w, err)
		return
	}
	if prev != nil && prev.Kind == "human" && prev.UserID != "" {
		s.notifyUser(r.Context(), prev.UserID, "unassigned",
			"You were unassigned: "+t.Title, "The task is back on the board.", "/board")
	}
	s.Publish("board", "task.published", map[string]any{"taskId": t.ID, "customerId": t.CustomerID})
	w.WriteHeader(http.StatusNoContent)
}

func (s *Server) handleAssignAI(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Context workperform.JobContext `json:"context"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	t, _, ok := s.consultantTask(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	if t.Status != domain.StatusPublished {
		writeError(w, http.StatusConflict, "bad_status", "only published tasks can be assigned to AI")
		return
	}
	if s.performerClient == nil {
		writeError(w, http.StatusServiceUnavailable, "performer_unavailable", "work performer is not configured")
		return
	}

	atts := make([]workperform.Attachment, 0, len(t.Attachments))
	for _, a := range t.Attachments {
		url := a.URL
		if a.FileID != "" {
			url = files.SignedURL(s.cfg.AppInternalURL, []byte(s.cfg.SessionSecret), a.FileID,
				time.Now().Add(files.DefaultTokenTTL))
		}
		atts = append(atts, workperform.Attachment{Name: a.Name, URL: url, MimeType: a.MimeType})
	}
	job, err := s.performerClient.Submit(r.Context(), workperform.JobRequest{
		TaskID:             t.ID,
		Title:              t.Title,
		Description:        t.Description,
		AcceptanceCriteria: t.AcceptanceCriteria,
		Attachments:        atts,
		Links:              t.Links,
		Context:            req.Context,
		CallbackURL:        s.cfg.AppInternalURL + "/api/v1/internal/work-results",
	})
	if err != nil {
		s.log.Error("submit wp job", "task", t.ID, "err", err)
		writeError(w, http.StatusBadGateway, "performer_error", "work performer rejected the job: "+err.Error())
		return
	}

	err = s.store.TransitionTask(r.Context(), t, domain.StatusAssigned,
		CurrentUser(r.Context()).ID, "assigned_to_ai",
		map[string]any{"jobId": job.JobID},
		map[string]any{"assignee": domain.Assignee{Kind: "ai", JobID: job.JobID}})
	if err != nil {
		// roll the job back as best we can
		if cErr := s.performerClient.Cancel(r.Context(), job.JobID); cErr != nil {
			s.log.Warn("cancel orphaned wp job", "jobId", job.JobID, "err", cErr)
		}
		s.taskTransitionError(w, err)
		return
	}
	s.Publish("board", "task.assigned", map[string]any{"taskId": t.ID, "customerId": t.CustomerID})
	writeJSON(w, http.StatusAccepted, map[string]string{"jobId": job.JobID})
}

func (s *Server) handleReviewQueue(w http.ResponseWriter, r *http.Request) {
	u := CurrentUser(r.Context())
	consultantID := u.ID
	if u.Roles.Admin {
		consultantID = ""
	}
	customers, err := s.store.ListCustomers(r.Context(), consultantID, false)
	if err != nil {
		s.internalError(w, r, "list customers", err)
		return
	}
	ids := make([]string, len(customers))
	for i, c := range customers {
		ids[i] = c.ID
	}
	tasks := []domain.Task{}
	if len(ids) > 0 {
		tasks, err = s.store.ListTasks(r.Context(), store.TaskFilter{
			CustomerIDs: ids,
			Statuses:    []domain.TaskStatus{domain.StatusInReview},
			Sort:        "updated",
			Limit:       200,
		})
		if err != nil {
			s.internalError(w, r, "list reviews", err)
			return
		}
	}
	writeJSON(w, http.StatusOK, map[string]any{"tasks": tasks})
}

func (s *Server) handleReview(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Decision  string `json:"decision"` // approve | request_changes
		Note      string `json:"note"`
		Checklist []struct {
			Criterion string `json:"criterion"`
			OK        bool   `json:"ok"`
		} `json:"checklist"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	t, _, ok := s.consultantTask(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	me := CurrentUser(r.Context())
	note := chat.SanitizePlain(req.Note)
	// §11.18: per-AC verdicts land on the timeline as the QA audit trail
	checklist := make([]map[string]any, 0, len(req.Checklist))
	for _, c := range req.Checklist {
		checklist = append(checklist, map[string]any{"criterion": c.Criterion, "ok": c.OK})
	}
	data := map[string]any{"note": note, "checklist": checklist}

	switch req.Decision {
	case "approve":
		err := s.store.TransitionTask(r.Context(), t, domain.StatusApproved, me.ID, "approved", data, nil)
		if err != nil {
			s.taskTransitionError(w, err)
			return
		}
		// §4.5: immutable award, human assignees only (AI earns no bounty)
		if t.Assignee != nil && t.Assignee.Kind == "human" && t.Assignee.UserID != "" {
			award := &store.BountyAward{
				TaskID:       t.ID,
				DeveloperID:  t.Assignee.UserID,
				ConsultantID: t.ConsultantID,
				CustomerID:   t.CustomerID,
				Amount:       t.Bounty,
				Coefficient:  t.EffortCoefficient,
			}
			if err := s.store.InsertAward(r.Context(), award); err != nil {
				s.log.Error("insert bounty award", "task", t.ID, "err", err)
			}
			s.notifyUser(r.Context(), t.Assignee.UserID, "approved",
				"Approved! Bounty awarded: "+formatBounty(t.Bounty), t.Title, "/tasks/"+t.ID)
		}
		s.metrics.Inc("tasks_approved_total", 1)
	case "request_changes":
		if strings.TrimSpace(note) == "" {
			writeError(w, http.StatusBadRequest, "note_required", "explain what needs to change")
			return
		}
		err := s.store.TransitionTask(r.Context(), t, domain.StatusChangesRequested, me.ID, "changes_requested", data, nil)
		if err != nil {
			s.taskTransitionError(w, err)
			return
		}
		if t.Assignee != nil && t.Assignee.Kind == "human" && t.Assignee.UserID != "" {
			s.notifyUser(r.Context(), t.Assignee.UserID, "changes_requested",
				"Changes requested: "+t.Title, note, "/tasks/"+t.ID)
		}
		s.metrics.Inc("tasks_changes_requested_total", 1)
	default:
		writeError(w, http.StatusBadRequest, "bad_decision", "decision must be approve or request_changes")
		return
	}
	s.Publish("board", "task.reviewed", map[string]any{"taskId": t.ID, "customerId": t.CustomerID})
	w.WriteHeader(http.StatusNoContent)
}

func (s *Server) handleListPool(w http.ResponseWriter, r *http.Request) {
	me := CurrentUser(r.Context())
	poolIDs, err := s.store.PoolDeveloperIDs(r.Context(), me.ID)
	if err != nil {
		s.internalError(w, r, "list pool", err)
		return
	}
	inPool := map[string]bool{}
	for _, id := range poolIDs {
		inPool[id] = true
	}
	// global developer pool (§3): all enabled developers
	cur, err := s.store.DB.Collection("users").Find(r.Context(),
		bson.M{"roles.developer": true, "disabled": false})
	if err != nil {
		s.internalError(w, r, "list developers", err)
		return
	}
	var devs []domain.User
	if err := cur.All(r.Context(), &devs); err != nil {
		s.internalError(w, r, "decode developers", err)
		return
	}
	out := make([]map[string]any, 0, len(devs))
	for _, d := range devs {
		out = append(out, map[string]any{
			"id": d.ID, "name": d.Name, "email": d.Email, "bio": d.Bio,
			"avatarFileId": d.AvatarFileID, "inPool": inPool[d.ID],
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{"developers": out})
}

func (s *Server) handleAddPool(w http.ResponseWriter, r *http.Request) {
	var req struct {
		DeveloperID string `json:"developerId"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	dev, err := s.store.UserByID(r.Context(), req.DeveloperID)
	if err != nil || !dev.Roles.Developer {
		writeError(w, http.StatusBadRequest, "not_developer", "that user is not a developer")
		return
	}
	me := CurrentUser(r.Context())
	if err := s.store.AddToPool(r.Context(), me.ID, req.DeveloperID); err != nil {
		if err == store.ErrDuplicate {
			writeError(w, http.StatusConflict, "already_in_pool", "developer is already in your pool")
			return
		}
		s.internalError(w, r, "add to pool", err)
		return
	}
	s.notifyUser(r.Context(), req.DeveloperID, "pool_added",
		me.Name+" added you to their developer pool",
		"Their published tasks now appear on your bounty board.", "/board")
	w.WriteHeader(http.StatusCreated)
}

func (s *Server) handleRemovePool(w http.ResponseWriter, r *http.Request) {
	me := CurrentUser(r.Context())
	if err := s.store.RemoveFromPool(r.Context(), me.ID, r.PathValue("developerId")); err != nil {
		if err == store.ErrNotFound {
			writeError(w, http.StatusNotFound, "not_found", "developer is not in your pool")
			return
		}
		s.internalError(w, r, "remove from pool", err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func formatBounty(v float64) string {
	return strconv.FormatFloat(v, 'f', -1, 64)
}
