package httpapi

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strconv"
	"time"

	"commilitia.net/cdrop/internal/clipboard"
	"commilitia.net/cdrop/internal/jwtauth"
)

type clipboardPutReq struct {
	Content     string `json:"content"`
	ContentType string `json:"content_type"`
	// OriginTs is the source device's copy time (ms). It's the LWW key: the write
	// is accepted only if it strictly exceeds the stored one. Clients that omit it
	// (legacy / transitional) get server-now, so they still sync.
	OriginTs int64 `json:"origin_ts"`
}

// handleClipboardPut 接受一次剪贴板写入。请求体最大字节限制（MaxBytesReader）
// 给上限 + JSON 信封冗余 (×1.5)；服务层会再核一次纯内容字节数。
func (s *Server) handleClipboardPut(w http.ResponseWriter, r *http.Request) {
	if s.clipboard == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "clipboard service unavailable"})
		return
	}

	claims, _ := jwtauth.ClaimsFromContext(r.Context())
	device, _ := jwtauth.DeviceNameFromContext(r.Context())

	maxBody := int64(s.clipboard.MaxBytes() + s.clipboard.MaxBytes()/2 + 256)
	r.Body = http.MaxBytesReader(w, r.Body, maxBody)

	var req clipboardPutReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		// MaxBytesReader 触发时返回 *http.MaxBytesError；统一映射 413。
		var mbe *http.MaxBytesError
		if errors.As(err, &mbe) {
			writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "request body too large"})
			return
		}
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}

	originTs := req.OriginTs
	if originTs <= 0 {
		originTs = time.Now().UnixMilli() // client didn't tag a copy time → use now
	}

	st, accepted, err := s.clipboard.Put(r.Context(), claims.UserID, req.ContentType, req.Content, device, originTs)
	switch {
	case errors.Is(err, clipboard.ErrUnsupportedType):
		writeJSON(w, http.StatusUnsupportedMediaType, map[string]string{"error": err.Error()})
		return
	case errors.Is(err, clipboard.ErrTooLarge):
		writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": err.Error()})
		return
	case err != nil:
		slog.Error("clipboard put failed", "err", err, "user", claims.UserID)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
		return
	}

	w.Header().Set("ETag", etagFor(st.Version))
	if !accepted {
		// LWW rejected this write — a newer copy already won. Hand back the current
		// winner so the client reconciles (pulls) instead of clobbering it.
		writeJSON(w, http.StatusConflict, st)
		return
	}
	writeJSON(w, http.StatusAccepted, st)
}

type clipboardMeta struct {
	Version      int64  `json:"version"`
	SourceDevice string `json:"source_device"`
	ContentType  string `json:"content_type"`
	UpdatedAt    int64  `json:"updated_at"`
	OriginTs     int64  `json:"origin_ts"`
}

// handleClipboardVersion 返回剪贴板版本元信息（不含 content）。给读不了 HTTP 状态
// 码的轮询客户端（iOS 快捷指令）做廉价变更探测：版本变了、且来源不是自己，才去
// GET 全量内容——避免每轮都全量拉取。
func (s *Server) handleClipboardVersion(w http.ResponseWriter, r *http.Request) {
	if s.clipboard == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "clipboard service unavailable"})
		return
	}
	claims, _ := jwtauth.ClaimsFromContext(r.Context())
	st, err := s.clipboard.Get(r.Context(), claims.UserID)
	if errors.Is(err, sql.ErrNoRows) {
		st = &clipboard.State{ContentType: clipboard.ContentTypeText}
	} else if err != nil {
		slog.Error("clipboard version get failed", "err", err, "user", claims.UserID)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
		return
	}

	etag := etagFor(st.Version)
	w.Header().Set("ETag", etag)
	if match := r.Header.Get("If-None-Match"); match != "" && match == etag {
		w.WriteHeader(http.StatusNotModified)
		return
	}
	writeJSON(w, http.StatusOK, clipboardMeta{
		Version:      st.Version,
		SourceDevice: st.SourceDevice,
		ContentType:  st.ContentType,
		UpdatedAt:    st.UpdatedAt,
		OriginTs:     st.OriginTs,
	})
}

// handleClipboardGet 返回当前剪贴板内容；支持 If-None-Match: <updated_at>
// → 304 节省流量。用户尚未写入时返回 200 + 空 State + ETag "0"，让"槽位常驻"
// 语义对前端更友好（避免 DevTools Network 红色 404）。
func (s *Server) handleClipboardGet(w http.ResponseWriter, r *http.Request) {
	if s.clipboard == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "clipboard service unavailable"})
		return
	}

	claims, _ := jwtauth.ClaimsFromContext(r.Context())
	st, err := s.clipboard.Get(r.Context(), claims.UserID)
	if errors.Is(err, sql.ErrNoRows) {
		st = &clipboard.State{ContentType: clipboard.ContentTypeText}
	} else if err != nil {
		slog.Error("clipboard get failed", "err", err, "user", claims.UserID)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
		return
	}

	etag := etagFor(st.Version)
	w.Header().Set("ETag", etag)
	if match := r.Header.Get("If-None-Match"); match != "" && match == etag {
		w.WriteHeader(http.StatusNotModified)
		return
	}
	writeJSON(w, http.StatusOK, st)
}

// etagFor 用版本号的十进制字符串当 ETag——版本是严格单调的，故 ETag 精确反映
// 「内容变了没有」（秒级 updated_at 会在同秒两写时撞号）。RFC 9110 的强 ETag。
func etagFor(version int64) string {
	return fmt.Sprintf("\"%s\"", strconv.FormatInt(version, 10))
}
