package http

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	stdhttp "net/http"
	"strings"
	"sync"
)

const maxGitoWebhookBodyBytes = 1 << 20

// GitoBranchUpdatedEvent is the decoded branch.updated payload the HTTP
// receiver passes to the configured handler. It carries only the fields
// needed to route and dedup the event at the HTTP boundary.
type GitoBranchUpdatedEvent struct {
	RepoID string
	Branch string
	Before string
	After  string
}

// GitoBranchEventHandler is the seam the HTTP receiver forwards a verified
// branch.updated event to. Satisfied by *gitosync.Bridge (via an adapter
// defined in main) and test fakes.
type GitoBranchEventHandler interface {
	Handle(ctx context.Context, ev GitoBranchUpdatedEvent) error
}

// gitoDeliveryStore is a process-lifetime idempotency store for HTTP-layer
// delivery-id and revision duplicate detection.
type gitoDeliveryStore struct {
	mu      sync.Mutex
	records map[string]struct{}
}

func newGitoDeliveryStore() *gitoDeliveryStore {
	return &gitoDeliveryStore{records: make(map[string]struct{})}
}

func (s *gitoDeliveryStore) hasSeen(key string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	_, ok := s.records[key]
	return ok
}

func (s *gitoDeliveryStore) markSeen(key string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.records[key] = struct{}{}
}

// gitoWebhookPayload is the minimal JSON structure expected in a Gito
// branch.updated HTTP webhook body. It includes optional fields used for
// contract validation (type, changed_files) without requiring full proto
// socket decoding.
type gitoWebhookPayload struct {
	Type         string      `json:"type"`
	RepoID       string      `json:"repo_id"`
	Branch       string      `json:"branch"`
	Before       string      `json:"before"`
	After        string      `json:"after"`
	ChangedFiles interface{} `json:"changed_files"`
}

// validateBranchUpdatedPayload validates that the decoded payload satisfies
// the branch.updated contract. Returns an error description when the payload
// is invalid. This mirrors the validation in
// gitoevents.DecodeBranchUpdatedPayload but works on the plain JSON struct
// to avoid import cycles.
func validateBranchUpdatedPayload(p *gitoWebhookPayload) error {
	// body type must be "branch.updated" if present
	if p.Type != "" && p.Type != "branch.updated" {
		return fmt.Errorf("payload type %q, want %q", p.Type, "branch.updated")
	}

	// repo_id and branch are required (trim whitespace before check)
	p.RepoID = strings.TrimSpace(p.RepoID)
	p.Branch = strings.TrimSpace(p.Branch)
	if p.RepoID == "" {
		return errors.New("payload missing required field repo_id")
	}
	if p.Branch == "" {
		return errors.New("payload missing required field branch")
	}

	// changed_files must be a list of objects if present
	if p.ChangedFiles != nil {
		changedFiles, ok := p.ChangedFiles.([]interface{})
		if !ok {
			return errors.New("payload changed_files is not a list")
		}
		for i, item := range changedFiles {
			if _, ok := item.(map[string]interface{}); !ok {
				return fmt.Errorf("payload changed_files[%d] is not an object", i)
			}
		}
	}

	return nil
}

func (h *Handler) ReceiveGitoWebhook(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	if !h.gitoConfig.ready() {
		writeError(w, stdhttp.StatusServiceUnavailable, "gito webhook secret is not configured")
		return
	}

	body, err := io.ReadAll(stdhttp.MaxBytesReader(w, r.Body, maxGitoWebhookBodyBytes))
	if err != nil {
		if errors.As(err, new(*stdhttp.MaxBytesError)) {
			writeError(w, stdhttp.StatusRequestEntityTooLarge, "webhook body is too large")
			return
		}
		writeError(w, stdhttp.StatusBadRequest, "invalid webhook body")
		return
	}

	signature := strings.TrimSpace(r.Header.Get("X-Gito-Signature"))
	if signature == "" || !validGitoWebhookSignature(h.gitoConfig.secret, body, signature) {
		writeError(w, stdhttp.StatusUnauthorized, "invalid gito webhook signature")
		return
	}

	event := strings.TrimSpace(r.Header.Get("X-Gito-Event"))
	if event != "branch.updated" {
		writeError(w, stdhttp.StatusBadRequest, "unexpected gito event type")
		return
	}
	deliveryID := strings.TrimSpace(r.Header.Get("X-Gito-Delivery"))
	if deliveryID == "" {
		writeError(w, stdhttp.StatusBadRequest, "missing gito delivery id")
		return
	}

	var p gitoWebhookPayload
	if err := json.Unmarshal(body, &p); err != nil {
		writeError(w, stdhttp.StatusBadRequest, "invalid JSON body")
		return
	}

	if err := validateBranchUpdatedPayload(&p); err != nil {
		writeError(w, stdhttp.StatusBadRequest, fmt.Sprintf("invalid branch.updated payload: %s", err.Error()))
		return
	}

	ev := GitoBranchUpdatedEvent{
		RepoID: p.RepoID,
		Branch: p.Branch,
		Before: strings.TrimSpace(p.Before),
		After:  strings.TrimSpace(p.After),
	}

	// Off-target events are acked without side effects.
	targetBranch := h.gitoConfig.branch
	if targetBranch == "" {
		targetBranch = "develop"
	}
	if ev.RepoID != h.gitoConfig.repoID || ev.Branch != targetBranch {
		writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status": "ignored"})
		return
	}

	// HTTP-layer idempotency: check delivery key first, then revision key
	// when after is set. Mark both after successful handling.
	var deliveryKey, revisionKey string
	deliveryKey = "delivery:" + deliveryID
	if h.gitoDelivery.hasSeen(deliveryKey) {
		writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status": "duplicate"})
		return
	}
	if ev.After != "" {
		revisionKey = "revision:" + ev.RepoID + ":" + ev.Branch + ":" + ev.After
		if h.gitoDelivery.hasSeen(revisionKey) {
			writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status": "duplicate"})
			return
		}
	}

	if h.gitoEventHandler != nil {
		if err := h.gitoEventHandler.Handle(r.Context(), ev); err != nil {
			// Do not mark keys on error so the caller can retry.
			if h.logger != nil {
				h.logger.Error("gito webhook handler failed", "error", err)
			}
			writeError(w, stdhttp.StatusInternalServerError, "internal server error")
			return
		}
	}

	h.gitoDelivery.markSeen(deliveryKey)
	if revisionKey != "" {
		h.gitoDelivery.markSeen(revisionKey)
	}

	writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status": "accepted"})
}

// validGitoWebhookSignature verifies a Gito HMAC-SHA256 webhook signature.
// Accepts both raw hex and the "sha256=<hex>" prefixed form.
func validGitoWebhookSignature(secret string, body []byte, signature string) bool {
	sig := signature
	if strings.HasPrefix(sig, "sha256=") {
		sig = sig[len("sha256="):]
	}

	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write(body)
	expected := mac.Sum(nil)

	actual, err := hex.DecodeString(sig)
	if err != nil {
		return false
	}
	return hmac.Equal(actual, expected)
}
