package httpx

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strings"
	"time"

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

	"bountyboard/internal/crypto"
	"bountyboard/internal/domain"
	"bountyboard/internal/store"
	syncpkg "bountyboard/internal/sync"
)

// customerPayload is the create/update body. Credentials are write-only:
// they are encrypted at rest and never returned.
type customerPayload struct {
	Name                 *string               `json:"name"`
	Type                 *domain.TicketingType `json:"type"`
	BaseURL              *string               `json:"baseUrl"`
	ProjectKey           *string               `json:"projectKey"`
	Credentials          *syncpkg.Credentials  `json:"credentials"`
	PollInterval         *int                  `json:"pollIntervalSec"`
	ConsultantIDs        *[]string             `json:"consultantIds"`
	ConsultantIdentities *map[string]string    `json:"consultantIdentities"`
	DefaultBudget        *float64              `json:"defaultBudget"`
	Archived             *bool                 `json:"archived"`
	Version              *int64                `json:"version"`
}

func (s *Server) sealCredentials(creds syncpkg.Credentials) (string, error) {
	plain, err := json.Marshal(creds)
	if err != nil {
		return "", err
	}
	return crypto.Seal(s.cfg.CredentialsEncKey, plain)
}

func (s *Server) openCredentials(c *domain.Customer) (syncpkg.Credentials, error) {
	if c.Ticketing.CredentialsEnc == "" {
		return syncpkg.Credentials{}, nil
	}
	plain, err := crypto.Open(s.cfg.CredentialsEncKey, c.Ticketing.CredentialsEnc)
	if err != nil {
		return nil, err
	}
	var creds syncpkg.Credentials
	if err := json.Unmarshal(plain, &creds); err != nil {
		return nil, err
	}
	return creds, nil
}

// customerJSON augments the customer with a hasCredentials flag (the
// ciphertext itself is never serialized).
func customerJSON(c *domain.Customer) map[string]any {
	return map[string]any{
		"id": c.ID, "name": c.Name,
		"ticketing": map[string]any{
			"type": c.Ticketing.Type, "baseUrl": c.Ticketing.BaseURL,
			"projectKey": c.Ticketing.ProjectKey, "pollIntervalSec": c.Ticketing.PollIntervalSec,
			"lastSyncAt": c.Ticketing.LastSyncAt, "lastSyncStatus": c.Ticketing.LastSyncStatus,
			"lastSyncError":        c.Ticketing.LastSyncError,
			"hasCredentials":       c.Ticketing.CredentialsEnc != "",
			"consultantIdentities": c.Ticketing.ConsultantIdentities,
		},
		"consultantIds": c.ConsultantIDs, "defaultBudget": c.DefaultBudget,
		"archived": c.Archived, "createdAt": c.CreatedAt, "updatedAt": c.UpdatedAt,
		"version": c.Version,
	}
}

func (s *Server) handleAdminListCustomers(w http.ResponseWriter, r *http.Request) {
	include := r.URL.Query().Get("includeArchived") == "true"
	customers, err := s.store.ListCustomers(r.Context(), "", include)
	if err != nil {
		s.internalError(w, r, "list customers", err)
		return
	}
	out := make([]map[string]any, len(customers))
	for i := range customers {
		out[i] = customerJSON(&customers[i])
	}
	writeJSON(w, http.StatusOK, map[string]any{"customers": out})
}

func (s *Server) handleAdminGetCustomer(w http.ResponseWriter, r *http.Request) {
	c, err := s.store.CustomerByID(r.Context(), r.PathValue("id"))
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			writeError(w, http.StatusNotFound, "not_found", "customer not found")
			return
		}
		s.internalError(w, r, "get customer", err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"customer": customerJSON(c)})
}

// cleanIdentities trims values and drops empty mappings so we don't persist
// blank usernames for consultants the admin left unmapped.
func cleanIdentities(in map[string]string) map[string]string {
	out := map[string]string{}
	for k, v := range in {
		if t := strings.TrimSpace(v); t != "" {
			out[k] = t
		}
	}
	return out
}

func (s *Server) validateConsultants(ctx context.Context, ids []string) (string, bool) {
	for _, id := range ids {
		u, err := s.store.UserByID(ctx, id)
		if err != nil || !u.Roles.Consultant {
			return id, false
		}
	}
	return "", true
}

func (s *Server) handleAdminCreateCustomer(w http.ResponseWriter, r *http.Request) {
	var req customerPayload
	if !decodeJSON(w, r, &req) {
		return
	}
	if req.Name == nil || strings.TrimSpace(*req.Name) == "" {
		writeError(w, http.StatusBadRequest, "invalid_name", "customer name is required")
		return
	}
	if req.Type == nil || !req.Type.Valid() {
		writeError(w, http.StatusBadRequest, "invalid_type", "ticketing type must be jira, azure_devops, youtrack, wekan or demo")
		return
	}
	settings, err := s.store.GetSettings(r.Context())
	if err != nil {
		s.internalError(w, r, "get settings", err)
		return
	}

	c := &domain.Customer{
		Name: strings.TrimSpace(*req.Name),
		Ticketing: domain.Ticketing{
			Type:            *req.Type,
			PollIntervalSec: settings.DefaultPollIntervalSec,
		},
		DefaultBudget: settings.DefaultCustomerBudget,
		ConsultantIDs: []string{},
	}
	if req.BaseURL != nil {
		c.Ticketing.BaseURL = strings.TrimRight(strings.TrimSpace(*req.BaseURL), "/")
	}
	if req.ProjectKey != nil {
		c.Ticketing.ProjectKey = strings.TrimSpace(*req.ProjectKey)
	}
	if req.PollInterval != nil && *req.PollInterval >= 10 {
		c.Ticketing.PollIntervalSec = *req.PollInterval
	}
	if req.DefaultBudget != nil {
		c.DefaultBudget = *req.DefaultBudget
	}
	if req.ConsultantIDs != nil {
		if bad, ok := s.validateConsultants(r.Context(), *req.ConsultantIDs); !ok {
			writeError(w, http.StatusBadRequest, "invalid_consultant", "not a consultant: "+bad)
			return
		}
		c.ConsultantIDs = *req.ConsultantIDs
	}
	if req.ConsultantIdentities != nil {
		c.Ticketing.ConsultantIdentities = cleanIdentities(*req.ConsultantIdentities)
	}
	creds := syncpkg.Credentials{}
	if req.Credentials != nil {
		creds = *req.Credentials
	}
	// Validate credential shape by constructing the connector.
	if _, err := syncpkg.NewConnector(c.Ticketing.Type, c.Ticketing.BaseURL, c.Ticketing.ProjectKey, creds); err != nil {
		writeError(w, http.StatusBadRequest, "invalid_credentials", err.Error())
		return
	}
	if c.Ticketing.CredentialsEnc, err = s.sealCredentials(creds); err != nil {
		s.internalError(w, r, "seal credentials", err)
		return
	}

	if err := s.store.CreateCustomer(r.Context(), c); err != nil {
		if errors.Is(err, store.ErrDuplicate) {
			writeError(w, http.StatusConflict, "name_taken", "a customer with this name exists")
			return
		}
		s.internalError(w, r, "create customer", err)
		return
	}
	s.audit(r, "customer.create", "customer", c.ID, map[string]any{"name": c.Name, "type": c.Ticketing.Type})
	writeJSON(w, http.StatusCreated, map[string]any{"customer": customerJSON(c)})
}

func (s *Server) handleAdminPatchCustomer(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	var req customerPayload
	if !decodeJSON(w, r, &req) {
		return
	}
	c, err := s.store.CustomerByID(r.Context(), id)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			writeError(w, http.StatusNotFound, "not_found", "customer not found")
			return
		}
		s.internalError(w, r, "get customer", err)
		return
	}

	set := bson.M{}
	diff := map[string]any{}
	if req.Name != nil && strings.TrimSpace(*req.Name) != "" {
		set["name"] = strings.TrimSpace(*req.Name)
		diff["name"] = set["name"]
	}
	if req.Type != nil {
		if !req.Type.Valid() {
			writeError(w, http.StatusBadRequest, "invalid_type", "bad ticketing type")
			return
		}
		set["ticketing.type"] = *req.Type
		diff["type"] = *req.Type
	}
	if req.BaseURL != nil {
		set["ticketing.baseUrl"] = strings.TrimRight(strings.TrimSpace(*req.BaseURL), "/")
		diff["baseUrl"] = set["ticketing.baseUrl"]
	}
	if req.ProjectKey != nil {
		set["ticketing.projectKey"] = strings.TrimSpace(*req.ProjectKey)
		diff["projectKey"] = set["ticketing.projectKey"]
	}
	if req.PollInterval != nil {
		if *req.PollInterval < 10 {
			writeError(w, http.StatusBadRequest, "bad_request", "poll interval must be >= 10s")
			return
		}
		set["ticketing.pollIntervalSec"] = *req.PollInterval
		diff["pollIntervalSec"] = *req.PollInterval
	}
	if req.ConsultantIDs != nil {
		if bad, ok := s.validateConsultants(r.Context(), *req.ConsultantIDs); !ok {
			writeError(w, http.StatusBadRequest, "invalid_consultant", "not a consultant: "+bad)
			return
		}
		set["consultantIds"] = *req.ConsultantIDs
		diff["consultantIds"] = *req.ConsultantIDs
	}
	if req.ConsultantIdentities != nil {
		set["ticketing.consultantIdentities"] = cleanIdentities(*req.ConsultantIdentities)
		diff["consultantIdentities"] = "updated"
	}
	if req.DefaultBudget != nil {
		set["defaultBudget"] = *req.DefaultBudget
		diff["defaultBudget"] = *req.DefaultBudget
	}
	if req.Archived != nil {
		set["archived"] = *req.Archived
		diff["archived"] = *req.Archived
	}
	if req.Credentials != nil {
		enc, err := s.sealCredentials(*req.Credentials)
		if err != nil {
			s.internalError(w, r, "seal credentials", err)
			return
		}
		set["ticketing.credentialsEnc"] = enc
		diff["credentials"] = "rotated"
	}
	if len(set) == 0 {
		writeError(w, http.StatusBadRequest, "empty_update", "no recognized fields")
		return
	}
	version := c.Version
	if req.Version != nil {
		version = *req.Version
	}
	if err := s.store.UpdateCustomer(r.Context(), id, version, set); err != nil {
		if errors.Is(err, store.ErrVersionConflict) {
			writeError(w, http.StatusConflict, "conflict", "customer was modified concurrently; reload")
			return
		}
		s.internalError(w, r, "update customer", err)
		return
	}
	s.audit(r, "customer.update", "customer", id, diff)
	fresh, err := s.store.CustomerByID(r.Context(), id)
	if err != nil {
		s.internalError(w, r, "reload customer", err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"customer": customerJSON(fresh)})
}

func (s *Server) handleAdminDeleteCustomer(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	n, err := s.store.CountTasksForCustomer(r.Context(), id)
	if err != nil {
		s.internalError(w, r, "count tasks", err)
		return
	}
	if n > 0 {
		writeError(w, http.StatusConflict, "has_tasks",
			"customer has imported tasks; archive it instead of deleting")
		return
	}
	if err := s.store.DeleteCustomer(r.Context(), id); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			writeError(w, http.StatusNotFound, "not_found", "customer not found")
			return
		}
		s.internalError(w, r, "delete customer", err)
		return
	}
	s.audit(r, "customer.delete", "customer", id, nil)
	w.WriteHeader(http.StatusNoContent)
}

// handleAdminTestConnection tests the stored configuration of an existing
// customer.
func (s *Server) handleAdminTestConnection(w http.ResponseWriter, r *http.Request) {
	c, err := s.store.CustomerByID(r.Context(), r.PathValue("id"))
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			writeError(w, http.StatusNotFound, "not_found", "customer not found")
			return
		}
		s.internalError(w, r, "get customer", err)
		return
	}
	creds, err := s.openCredentials(c)
	if err != nil {
		s.internalError(w, r, "decrypt credentials", err)
		return
	}
	s.runConnectionTest(w, r, c.Ticketing.Type, c.Ticketing.BaseURL, c.Ticketing.ProjectKey, creds)
}

// handleAdminTestConnectionUnsaved tests credentials supplied in the wizard
// before the customer is saved.
func (s *Server) handleAdminTestConnectionUnsaved(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Type        domain.TicketingType `json:"type"`
		BaseURL     string               `json:"baseUrl"`
		ProjectKey  string               `json:"projectKey"`
		Credentials syncpkg.Credentials  `json:"credentials"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	s.runConnectionTest(w, r, req.Type, strings.TrimRight(req.BaseURL, "/"), req.ProjectKey, req.Credentials)
}

func (s *Server) runConnectionTest(w http.ResponseWriter, r *http.Request,
	t domain.TicketingType, baseURL, projectKey string, creds syncpkg.Credentials) {
	conn, err := syncpkg.NewConnector(t, baseURL, projectKey, creds)
	if err != nil {
		writeJSON(w, http.StatusOK, map[string]any{"ok": false, "error": err.Error()})
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
	defer cancel()
	start := time.Now()
	if err := conn.TestConnection(ctx); err != nil {
		writeJSON(w, http.StatusOK, map[string]any{"ok": false, "error": err.Error(),
			"latencyMs": time.Since(start).Milliseconds()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true,
		"latencyMs": time.Since(start).Milliseconds()})
}

func (s *Server) handleAdminSyncNow(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	if _, err := s.store.CustomerByID(r.Context(), id); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			writeError(w, http.StatusNotFound, "not_found", "customer not found")
			return
		}
		s.internalError(w, r, "get customer", err)
		return
	}
	if s.syncTrigger == nil {
		writeError(w, http.StatusServiceUnavailable, "sync_unavailable", "sync manager is not running")
		return
	}
	s.syncTrigger(id)
	s.audit(r, "customer.sync-now", "customer", id, nil)
	writeJSON(w, http.StatusAccepted, map[string]string{"status": "queued"})
}
