package handlers

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

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

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

type WebhookHandler struct{ db *xorm.Engine }

func NewWebhookHandler(db *xorm.Engine) *WebhookHandler { return &WebhookHandler{db: db} }

type webhookResponse struct {
	ID              int64   `json:"id"`
	Title           string  `json:"title"`
	URL             string  `json:"url"`
	Events          string  `json:"events"`
	Active          bool    `json:"active"`
	HasSecret       bool    `json:"hasSecret"`
	LastStatus      int     `json:"lastStatus"`
	LastDeliveredAt *string `json:"lastDeliveredAt"`
	CreatedAt       string  `json:"createdAt"`
}

func toWebhookResp(wh models.Webhook) webhookResponse {
	var last *string
	if wh.LastDeliveredAt != nil {
		s := wh.LastDeliveredAt.Format(time.RFC3339)
		last = &s
	}
	return webhookResponse{
		ID:              wh.ID,
		Title:           wh.Title,
		URL:             wh.URL,
		Events:          wh.Events,
		Active:          wh.Active,
		HasSecret:       wh.Secret != "",
		LastStatus:      wh.LastStatus,
		LastDeliveredAt: last,
		CreatedAt:       wh.CreatedAt.Format(time.RFC3339),
	}
}

func (h *WebhookHandler) resolveRepo(w http.ResponseWriter, r *http.Request) (*models.Repository, bool) {
	return resolveRepo(h.db, w, r)
}

func (h *WebhookHandler) canManage(repo *models.Repository, callerID int64) bool {
	if callerID == repo.OwnerID {
		return true
	}
	var m models.RepoMember
	found, _ := h.db.Where("repo_id = ? AND user_id = ? AND permission = 'admin'", repo.ID, callerID).Get(&m)
	return found
}

func (h *WebhookHandler) List(w http.ResponseWriter, r *http.Request) {
	repo, ok := h.resolveRepo(w, r)
	if !ok {
		return
	}
	var hooks []models.Webhook
	h.db.Where("repo_id = ?", repo.ID).OrderBy("created_at").Find(&hooks)
	resp := make([]webhookResponse, len(hooks))
	for i, wh := range hooks {
		resp[i] = toWebhookResp(wh)
	}
	jsonOK(w, resp)
}

func (h *WebhookHandler) Create(w http.ResponseWriter, r *http.Request) {
	repo, ok := h.resolveRepo(w, r)
	if !ok {
		return
	}
	callerID, _ := middleware.UserIDFromContext(r.Context())
	if !h.canManage(repo, callerID) {
		jsonError(w, "forbidden", http.StatusForbidden)
		return
	}

	var body struct {
		Title  string `json:"title"`
		URL    string `json:"url"`
		Secret string `json:"secret"`
		Events string `json:"events"`
		Active bool   `json:"active"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.URL == "" {
		jsonError(w, "url is required", http.StatusBadRequest)
		return
	}
	if body.Events == "" {
		body.Events = "push"
	}

	wh := &models.Webhook{
		RepoID: repo.ID, Title: body.Title, URL: body.URL,
		Secret: body.Secret, Events: body.Events, Active: body.Active,
	}
	if _, err := h.db.Insert(wh); err != nil {
		jsonError(w, "could not create webhook", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(toWebhookResp(*wh))
}

func (h *WebhookHandler) Update(w http.ResponseWriter, r *http.Request) {
	repo, ok := h.resolveRepo(w, r)
	if !ok {
		return
	}
	callerID, _ := middleware.UserIDFromContext(r.Context())
	if !h.canManage(repo, callerID) {
		jsonError(w, "forbidden", http.StatusForbidden)
		return
	}

	whID, _ := strconv.ParseInt(chi.URLParam(r, "whID"), 10, 64)
	var wh models.Webhook
	if found, _ := h.db.Where("id = ? AND repo_id = ?", whID, repo.ID).Get(&wh); !found {
		jsonError(w, "webhook not found", http.StatusNotFound)
		return
	}

	var body struct {
		Title  string `json:"title"`
		URL    string `json:"url"`
		Secret string `json:"secret"`
		Events string `json:"events"`
		Active bool   `json:"active"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		jsonError(w, "invalid body", http.StatusBadRequest)
		return
	}
	wh.Title = body.Title
	wh.URL = body.URL
	if body.Secret != "" {
		wh.Secret = body.Secret
	}
	wh.Events = body.Events
	wh.Active = body.Active
	h.db.ID(wh.ID).Cols("title", "url", "secret", "events", "active").Update(&wh)
	jsonOK(w, toWebhookResp(wh))
}

func (h *WebhookHandler) Delete(w http.ResponseWriter, r *http.Request) {
	repo, ok := h.resolveRepo(w, r)
	if !ok {
		return
	}
	callerID, _ := middleware.UserIDFromContext(r.Context())
	if !h.canManage(repo, callerID) {
		jsonError(w, "forbidden", http.StatusForbidden)
		return
	}
	whID, _ := strconv.ParseInt(chi.URLParam(r, "whID"), 10, 64)
	h.db.Where("id = ? AND repo_id = ?", whID, repo.ID).Delete(&models.Webhook{})
	w.WriteHeader(http.StatusNoContent)
}

func (h *WebhookHandler) Test(w http.ResponseWriter, r *http.Request) {
	repo, ok := h.resolveRepo(w, r)
	if !ok {
		return
	}
	callerID, _ := middleware.UserIDFromContext(r.Context())
	if !h.canManage(repo, callerID) {
		jsonError(w, "forbidden", http.StatusForbidden)
		return
	}
	whID, _ := strconv.ParseInt(chi.URLParam(r, "whID"), 10, 64)
	var wh models.Webhook
	if found, _ := h.db.Where("id = ? AND repo_id = ?", whID, repo.ID).Get(&wh); !found {
		jsonError(w, "webhook not found", http.StatusNotFound)
		return
	}

	payload := map[string]interface{}{
		"event": "ping",
		"repository": map[string]interface{}{
			"id": repo.ID, "name": repo.Name,
		},
		"zen": "Keep it simple.",
	}
	status := deliverWebhook(wh, payload)
	jsonOK(w, map[string]interface{}{"status": status, "ok": status >= 200 && status < 300})
}

// ── delivery ──────────────────────────────────────────────────────────────────

func deliverWebhook(wh models.Webhook, payload interface{}) int {
	body, err := json.Marshal(payload)
	if err != nil {
		return 0
	}

	req, err := http.NewRequest("POST", wh.URL, bytes.NewReader(body))
	if err != nil {
		return 0
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-ForgeBucket-Event", fmt.Sprintf("%v", payload.(map[string]interface{})["event"]))
	req.Header.Set("X-ForgeBucket-Delivery", strconv.FormatInt(time.Now().UnixNano(), 36))

	if wh.Secret != "" {
		mac := hmac.New(sha256.New, []byte(wh.Secret))
		mac.Write(body)
		req.Header.Set("X-ForgeBucket-Signature-256", "sha256="+hex.EncodeToString(mac.Sum(nil)))
	}

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return 0
	}
	defer resp.Body.Close()
	return resp.StatusCode
}

// FireWebhooks sends event payloads to all active webhooks for a repo that match the event.
// Called from other handlers; runs deliveries in a background goroutine.
func FireWebhooks(db *xorm.Engine, repoID int64, event string, payload map[string]interface{}) {
	var hooks []models.Webhook
	db.Where("repo_id = ? AND active = ?", repoID, true).Find(&hooks)

	for _, wh := range hooks {
		if !strings.Contains(","+wh.Events+",", ","+event+",") {
			continue
		}
		wh := wh // capture
		payload["event"] = event
		go func() {
			status := deliverWebhook(wh, payload)
			now := time.Now()
			wh.LastStatus = status
			wh.LastDeliveredAt = &now
			db.ID(wh.ID).Cols("last_status", "last_delivered_at").Update(&wh)
		}()
	}
}
