package webhook

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"net/http"

	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
	"github.com/go-chi/chi/v5"
)

// Handler handles webhook CRUD and execution routes.
type Handler struct {
	db  *sql.DB
	hub *gateway.Hub
}

// NewHandler creates a new webhook Handler.
func NewHandler(db *sql.DB, hub *gateway.Hub) *Handler {
	return &Handler{db: db, hub: hub}
}

// RegisterRoutes registers authenticated webhook routes.
//
//	POST /channels/{channelID}/webhooks  - create webhook
//	GET  /channels/{channelID}/webhooks  - list webhooks for channel
//	DELETE /webhooks/{webhookID}          - delete webhook
func (h *Handler) RegisterRoutes(r chi.Router) {
	r.Post("/", h.Create)
	r.Get("/", h.List)
}

// RegisterPublicRoutes registers the public webhook execution route (no auth).
//
//	POST /webhooks/{webhookID}/{token}   - execute webhook
func (h *Handler) RegisterPublicRoutes(r chi.Router) {
	r.Post("/webhooks/{webhookID}/{token}", h.Execute)
}

// ---- types ----

type webhookResponse struct {
	ID        string  `json:"id"`
	ChannelID string  `json:"channel_id"`
	Name      string  `json:"name"`
	Avatar    *string `json:"avatar"`
	CreatedBy string  `json:"created_by"`
	CreatedAt string  `json:"created_at"`
}

type webhookWithToken struct {
	webhookResponse
	Token string `json:"token"`
}

type createWebhookRequest struct {
	Name   string  `json:"name"`
	Avatar *string `json:"avatar"`
}

type executeWebhookRequest struct {
	Content  string  `json:"content"`
	Username *string `json:"username"`
	Avatar   *string `json:"avatar"`
}

// ---- helpers ----

func writeJSON(w http.ResponseWriter, status int, v interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(v)
}

func writeErr(w http.ResponseWriter, status int, msg string) {
	http.Error(w, `{"error":"`+msg+`"}`, status)
}

// ---- handlers ----

// @Summary Create a webhook
// @Description Create a new webhook for a channel
// @Tags webhooks
// @Accept json
// @Produce json
// @Security SessionAuth
// @Param channelID path string true "Channel ID"
// @Param body body createWebhookRequest true "Webhook data"
// @Success 201 {object} webhookWithToken
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /channels/{channelID}/webhooks [post]
// Create creates a new webhook for a channel.
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		writeErr(w, http.StatusUnauthorized, "unauthorized")
		return
	}

	channelID := chi.URLParam(r, "channelID")

	// Verify channel exists and user is a member of its server
	serverID, err := h.serverIDForChannel(r.Context(), channelID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			writeErr(w, http.StatusNotFound, "channel not found")
		} else {
			writeErr(w, http.StatusInternalServerError, "server error")
		}
		return
	}

	member, err := h.isMember(r.Context(), userID, serverID)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "server error")
		return
	}
	if !member {
		writeErr(w, http.StatusForbidden, "not a member of this server")
		return
	}

	var req createWebhookRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, http.StatusBadRequest, "invalid request")
		return
	}
	if req.Name == "" {
		writeErr(w, http.StatusBadRequest, "name is required")
		return
	}

	token := genToken()
	tokenHash := hashToken(token)

	var wh webhookWithToken
	var avatar sql.NullString
	var createdAt sql.NullString
	err = h.db.QueryRowContext(r.Context(), `
		INSERT INTO webhooks (channel_id, name, token_hash, avatar, created_by)
		VALUES ($1, $2, $3, $4, $5)
		RETURNING id, channel_id, name, avatar, created_by, created_at::text
	`, channelID, req.Name, tokenHash, req.Avatar, userID).Scan(
		&wh.ID, &wh.ChannelID, &wh.Name, &avatar, &wh.CreatedBy, &createdAt,
	)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "failed to create webhook")
		return
	}
	if avatar.Valid {
		wh.Avatar = &avatar.String
	}
	wh.CreatedAt = createdAt.String
	wh.Token = token

	writeJSON(w, http.StatusCreated, wh)
}

// @Summary List webhooks
// @Description List webhooks for a channel
// @Tags webhooks
// @Produce json
// @Security SessionAuth
// @Param channelID path string true "Channel ID"
// @Success 200 {array} webhookResponse
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /channels/{channelID}/webhooks [get]
// List returns all webhooks for a channel.
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		writeErr(w, http.StatusUnauthorized, "unauthorized")
		return
	}

	channelID := chi.URLParam(r, "channelID")

	serverID, err := h.serverIDForChannel(r.Context(), channelID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			writeErr(w, http.StatusNotFound, "channel not found")
		} else {
			writeErr(w, http.StatusInternalServerError, "server error")
		}
		return
	}

	member, err := h.isMember(r.Context(), userID, serverID)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "server error")
		return
	}
	if !member {
		writeErr(w, http.StatusForbidden, "not a member of this server")
		return
	}

	rows, err := h.db.QueryContext(r.Context(), `
		SELECT id, channel_id, name, avatar, created_by, created_at::text
		FROM webhooks WHERE channel_id = $1 ORDER BY created_at DESC
	`, channelID)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "server error")
		return
	}
	defer rows.Close()

	webhooks := make([]webhookResponse, 0)
	for rows.Next() {
		var wh webhookResponse
		var avatar sql.NullString
		var createdAt sql.NullString
		if err := rows.Scan(&wh.ID, &wh.ChannelID, &wh.Name, &avatar, &wh.CreatedBy, &createdAt); err != nil {
			writeErr(w, http.StatusInternalServerError, "server error")
			return
		}
		if avatar.Valid {
			wh.Avatar = &avatar.String
		}
		wh.CreatedAt = createdAt.String
		webhooks = append(webhooks, wh)
	}
	if err := rows.Err(); err != nil {
		writeErr(w, http.StatusInternalServerError, "server error")
		return
	}

	writeJSON(w, http.StatusOK, webhooks)
}

// @Summary Delete a webhook
// @Description Delete a webhook
// @Tags webhooks
// @Security SessionAuth
// @Param webhookID path string true "Webhook ID"
// @Success 204
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /webhooks/{webhookID} [delete]
// Delete removes a webhook.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		writeErr(w, http.StatusUnauthorized, "unauthorized")
		return
	}

	webhookID := chi.URLParam(r, "webhookID")

	var createdBy string
	err := h.db.QueryRowContext(r.Context(), `
		SELECT created_by FROM webhooks WHERE id = $1
	`, webhookID).Scan(&createdBy)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			writeErr(w, http.StatusNotFound, "webhook not found")
		} else {
			writeErr(w, http.StatusInternalServerError, "server error")
		}
		return
	}
	if createdBy != userID {
		writeErr(w, http.StatusNotFound, "webhook not found")
		return
	}

	_, err = h.db.ExecContext(r.Context(), `DELETE FROM webhooks WHERE id = $1`, webhookID)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "failed to delete webhook")
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// @Summary Execute a webhook
// @Description Execute a webhook to send a message (no auth required)
// @Tags webhooks
// @Accept json
// @Produce json
// @Param webhookID path string true "Webhook ID"
// @Param token path string true "Webhook token"
// @Param body body executeWebhookRequest true "Message data"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /webhooks/{webhookID}/{token} [post]
// Execute sends a message to a channel via webhook (no auth required).
func (h *Handler) Execute(w http.ResponseWriter, r *http.Request) {
	webhookID := chi.URLParam(r, "webhookID")
	token := chi.URLParam(r, "token")

	// Look up webhook and verify token via hash comparison
	var storedHash, channelID, webhookName string
	var webhookAvatar sql.NullString
	err := h.db.QueryRowContext(r.Context(), `
		SELECT token_hash, channel_id, name, avatar FROM webhooks WHERE id = $1
	`, webhookID).Scan(&storedHash, &channelID, &webhookName, &webhookAvatar)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			writeErr(w, http.StatusNotFound, "webhook not found")
		} else {
			writeErr(w, http.StatusInternalServerError, "server error")
		}
		return
	}

	if hashToken(token) != storedHash {
		writeErr(w, http.StatusUnauthorized, "invalid token")
		return
	}

	var req executeWebhookRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, http.StatusBadRequest, "invalid request")
		return
	}
	if req.Content == "" {
		writeErr(w, http.StatusBadRequest, "content is required")
		return
	}

	// Determine display name and avatar for the message author
	displayName := webhookName
	if req.Username != nil && *req.Username != "" {
		displayName = *req.Username
	}
	avatarURL := ""
	if req.Avatar != nil && *req.Avatar != "" {
		avatarURL = *req.Avatar
	} else if webhookAvatar.Valid {
		avatarURL = webhookAvatar.String
	}

	// Insert the message. We use the webhook_id as the author_id marker.
	// For bots/webhooks, author_id is set to a special value; alternatively
	// we can store it as the webhook id (which is a UUID but not a user).
	// The existing schema requires author_id FK to users, so we store the
	// webhook owner's created_by as the author to satisfy the FK constraint.
	var createdBy string
	err = h.db.QueryRowContext(r.Context(), `
		SELECT created_by FROM webhooks WHERE id = $1
	`, webhookID).Scan(&createdBy)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "server error")
		return
	}

	// Create the message
	type msgResp struct {
		ID          string  `json:"id"`
		ChannelID   string  `json:"channel_id"`
		AuthorID    string  `json:"author_id"`
		AuthorName  string  `json:"author_username"`
		DisplayName *string `json:"author_display_name"`
		Content     string  `json:"content"`
		EditedAt    *string `json:"edited_at"`
		CreatedAt   string  `json:"created_at"`
	}

	var msg msgResp
	var editedAt sql.NullString
	var createdAt sql.NullString
	err = h.db.QueryRowContext(r.Context(), `
		INSERT INTO messages (channel_id, author_id, content)
		VALUES ($1, $2, $3)
		RETURNING id, channel_id, author_id, content, edited_at::text, created_at::text
	`, channelID, createdBy, req.Content).Scan(
		&msg.ID, &msg.ChannelID, &msg.AuthorID, &msg.Content, &editedAt, &createdAt,
	)
	if err != nil {
		writeErr(w, http.StatusInternalServerError, "failed to send message")
		return
	}
	if editedAt.Valid {
		msg.EditedAt = &editedAt.String
	}
	msg.CreatedAt = createdAt.String

	// Override author display info with webhook identity
	msg.AuthorName = displayName
	if avatarURL != "" {
		msg.DisplayName = &displayName
	} else {
		msg.DisplayName = &displayName
	}

	// Broadcast MESSAGE_CREATE via gateway (scoped to server)
	if h.hub != nil {
		serverID, _ := h.hub.ServerIDForChannel(r.Context(), channelID)
		if serverID != "" {
			h.hub.BroadcastToServer(serverID, gateway.Event{
				Type: gateway.EventMessageCreate,
				Data: msg,
			})
		}
	}

	writeJSON(w, http.StatusCreated, msg)
}

// ---- internal helpers ----

func (h *Handler) serverIDForChannel(ctx context.Context, channelID string) (string, error) {
	var serverID string
	err := h.db.QueryRowContext(ctx, `SELECT server_id FROM channels WHERE id = $1`, channelID).Scan(&serverID)
	return serverID, err
}

func (h *Handler) isMember(ctx context.Context, userID, serverID string) (bool, error) {
	var exists bool
	err := h.db.QueryRowContext(ctx, `
		SELECT EXISTS(SELECT 1 FROM members WHERE user_id = $1 AND server_id = $2)
	`, userID, serverID).Scan(&exists)
	return exists, err
}
