package api

import (
	"errors"
	"net/http"
	"time"

	"github.com/xmatic-squad/bare/internal/auth"
	"github.com/xmatic-squad/bare/internal/store"
)

// deviceID — тело и ответ POST /api/devices: идентификатор выдаёт клиент
// (ADR-017), сервер только проверяет форму и принадлежность.
type deviceID struct {
	ID string `json:"id"`
}

// POST /api/devices — регистрация устройства. Уже заведённое своё —
// 200 и обновлённый last_seen; занятое чужим — 409, клиент берёт новый id.
func (s *server) createDevice(w http.ResponseWriter, r *http.Request) {
	var in deviceID
	if !decode(w, r, &in) {
		return
	}
	if !validID(in.ID) {
		Invalid(w, "id", "id — не 16 байт base64url")
		return
	}
	sess, _ := auth.From(r)
	created, err := s.st.RegisterDevice(r.Context(), in.ID, sess.Nick, sess.TokenHash, time.Now().UnixMilli())
	if errors.Is(err, store.ErrDeviceTaken) {
		Error(w, http.StatusConflict, "device_conflict", "такое устройство уже есть")
		return
	}
	if err != nil {
		s.internal(w, r, err)
		return
	}
	status := http.StatusOK
	if created {
		status = http.StatusCreated
	}
	writeJSON(w, status, deviceID{in.ID})
}

// deviceOut — строка ответа GET /api/devices.
type deviceOut struct {
	ID        string `json:"id"`
	CreatedAt int64  `json:"createdAt"`
	LastSeen  int64  `json:"lastSeen"`
	HasPush   bool   `json:"hasPush"`
	Current   bool   `json:"current"`
}

// GET /api/devices — устройства аккаунта. Самой push-подписки в ответе
// нет, только факт её наличия.
func (s *server) devices(w http.ResponseWriter, r *http.Request) {
	sess, _ := auth.From(r)
	list, err := s.st.Devices(r.Context(), sess.Nick)
	if err != nil {
		s.internal(w, r, err)
		return
	}
	out := make([]deviceOut, 0, len(list))
	for _, d := range list {
		out = append(out, deviceOut{
			ID:        d.ID,
			CreatedAt: d.CreatedAt,
			LastSeen:  d.LastSeen,
			HasPush:   d.HasPush,
			Current:   d.ID == sess.DeviceID,
		})
	}
	writeJSON(w, http.StatusOK, out)
}

// DELETE /api/devices/{id} — удаление устройства: очередь, подписка
// и сессии уходят каскадом, открытый поток событий закрывается.
//
// Чужое и несуществующее устройство отвечают тем же 204: удалять нечего,
// а отдельного кода на этот случай в протоколе нет (docs/protocol.md).
func (s *server) deleteDevice(w http.ResponseWriter, r *http.Request) {
	sess, _ := auth.From(r)
	id := r.PathValue("id")
	deleted, err := s.st.DeleteDevice(r.Context(), id, sess.Nick)
	if err != nil {
		s.internal(w, r, err)
		return
	}
	if deleted {
		s.hub.Close(id)
	}
	noContent(w)
}

// device читает X-Device и проверяет, что устройство принадлежит
// пользователю сессии. Заголовка нет, форма кривая, устройство чужое —
// всё это 403 unknown_device (docs/protocol.md, «Общие правила»).
func (s *server) device(w http.ResponseWriter, r *http.Request) (string, bool) {
	sess, _ := auth.From(r)
	id := r.Header.Get("X-Device")
	if !validID(id) {
		unknownDevice(w)
		return "", false
	}
	owned, err := s.st.DeviceOwned(r.Context(), id, sess.Nick)
	if err != nil {
		s.internal(w, r, err)
		return "", false
	}
	if !owned {
		unknownDevice(w)
		return "", false
	}
	return id, true
}

// optionalDevice — то же для маршрутов, где заголовок необязателен:
// он всего лишь просит не возвращать эхо отправившему устройству. Пустой
// X-Device — пусто, непустой обязан быть своим устройством: правило
// принадлежности общее для всех маршрутов, где устройство важно
// (docs/protocol.md, «Общие правила»).
func (s *server) optionalDevice(w http.ResponseWriter, r *http.Request) (string, bool) {
	if r.Header.Get("X-Device") == "" {
		return "", true
	}
	return s.device(w, r)
}

func unknownDevice(w http.ResponseWriter) {
	Error(w, http.StatusForbidden, "unknown_device", "это устройство не ваше")
}
