package api

import (
	"encoding/json"
	"net/http"
	"time"

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

// clockSkew — на сколько метка времени ULID вправе разойтись с часами
// сервера (ADR-017).
const clockSkew = 5 * time.Minute

// dmKeyID — keyId личного чата: ключ выводится из ECDH, идентификатора
// у него нет (docs/crypto.md, «Сообщение»).
const dmKeyID = "dm"

// maxAck — сколько идентификаторов принимает один ACK.
const maxAck = 500

// target — адресат конверта: ровно одно из двух.
type target struct {
	DM   string `json:"dm,omitempty"`
	Room string `json:"room,omitempty"`
}

// envelope — конверт из docs/protocol.md. Порядок полей — как в нём.
// from и ts ставит сервер: клиентские значения не читаются вовсе (ADR-017).
type envelope struct {
	ID    string `json:"id"`
	To    target `json:"to"`
	From  string `json:"from"`
	KeyID string `json:"keyId"`
	IV    string `json:"iv"`
	CT    string `json:"ct"`
	TS    int64  `json:"ts"`
}

// messageIn — тело POST /api/messages. Полей from и ts здесь нет
// намеренно: что бы клиент ни прислал, сервер ставит своё (ADR-017).
type messageIn struct {
	ID    string `json:"id"`
	To    target `json:"to"`
	KeyID string `json:"keyId"`
	IV    string `json:"iv"`
	CT    string `json:"ct"`
}

// POST /api/messages — отправка. Сервер не умеет проверять шифротекст,
// он проверяет форму и раскладывает конверт по очередям (ADR-008).
// Порядок проверок — docs/protocol.md, «Сообщения».
func (s *server) sendMessage(w http.ResponseWriter, r *http.Request) {
	var in messageIn
	if !decode(w, r, &in) {
		return
	}
	ms, ok := checkForm(w, in)
	if !ok {
		return
	}

	now := time.Now()
	if d := now.Sub(time.UnixMilli(ms)); d > clockSkew || d < -clockSkew {
		Error(w, http.StatusBadRequest, "clock_skew",
			"проверьте часы на устройстве: расхождение больше 5 минут")
		return
	}

	// Принадлежность устройства — право, а не форма, поэтому проверяется
	// после разбора тела: кривое тело отвечает bad_json и invalid даже
	// с чужим X-Device (ADR-043).
	device, ok := s.device(w, r)
	if !ok {
		return
	}

	sess, _ := auth.From(r)
	room := in.To.Room != ""
	// Заголовок и адрес чата для пуша: сервер собирает их из того, что
	// и так знает, — из ников и имени комнаты (ADR-023).
	var signal push.Payload
	if room {
		access, err := s.st.RoomAccess(r.Context(), in.To.Room, sess.Nick, in.KeyID)
		if err != nil {
			s.internal(w, r, err)
			return
		}
		if !access.Member {
			Error(w, http.StatusForbidden, "not_member", "вы не участник комнаты")
			return
		}
		if !access.KnownKey {
			Error(w, http.StatusBadRequest, "unknown_key", "у комнаты нет такого ключа")
			return
		}
		signal = push.Payload{Title: "#" + access.Name, Chat: "room:" + in.To.Room}
	} else {
		if _, ok := s.peer(w, r, in.To.DM, sess.Nick); !ok {
			return
		}
		signal = push.Payload{Title: "@" + sess.Nick, Chat: "dm:" + sess.Nick}
	}

	if wait, ok := s.msgs.take(sess.Nick, now); !ok {
		s.rateLimited(w, wait)
		return
	}

	env := envelope{
		ID:    in.ID,
		To:    target{DM: in.To.DM, Room: in.To.Room},
		From:  sess.Nick,
		KeyID: in.KeyID,
		IV:    in.IV,
		CT:    in.CT,
		TS:    now.UnixMilli(),
	}
	raw, err := json.Marshal(env)
	if err != nil {
		s.internal(w, r, err)
		return
	}
	delivery := store.Delivery{
		From:     env.From,
		To:       env.To.DM,
		Room:     env.To.Room,
		Exclude:  device,
		MsgID:    env.ID,
		Envelope: string(raw),
		Now:      env.TS,
	}
	var devices []store.Target
	if room {
		devices, err = s.st.DeliverRoom(r.Context(), delivery)
	} else {
		devices, err = s.st.DeliverDM(r.Context(), delivery)
	}
	if err != nil {
		s.internal(w, r, err)
		return
	}
	// Очередь уже записана: подключённое устройство получает конверт
	// сразу, остальные — при подключении.
	for _, target := range devices {
		s.hub.Send(target.ID, hub.Event{Name: "msg", Data: string(raw)})
	}
	// Пуш — побочный эффект доставки, а не её часть: конверт уже
	// в очереди, и ответ на запрос отправку пуша не ждёт (ADR-023).
	s.push.Send(s.silent(devices, env.From), signal)
	writeJSON(w, http.StatusAccepted, struct {
		ID string `json:"id"`
		TS int64  `json:"ts"`
	}{env.ID, env.TS})
}

// silent — устройства, которым нужен пуш: чужие (устройства отправителя
// пуша не получают, ADR-045), подписанные и молчащие — те, что не держат
// поток событий (ADR-023).
//
// Устройство без подписки отсеивается здесь: отправить ему нечего,
// а место в очереди отправки оно заняло бы (ADR-048). Проверка на
// подключение — ранний отсев: решает её повтор в момент захвата права
// на пуш, потому что между этой строкой и отправкой устройство успевает
// подключиться (ADR-023).
func (s *server) silent(targets []store.Target, from string) []push.Target {
	var out []push.Target
	for _, target := range targets {
		if target.Nick == from || !target.HasPush || s.hub.Connected(target.ID) {
			continue
		}
		out = append(out, push.Target{Device: target.ID, Owner: target.Nick})
	}
	return out
}

// checkForm проверяет форму полей конверта (docs/crypto.md, «Что сервер
// проверяет») и отдаёт метку времени из ULID. Ответ об ошибке уже написан,
// если вернулось false.
func checkForm(w http.ResponseWriter, in messageIn) (int64, bool) {
	ms, ok := ulidTime(in.ID)
	if !ok {
		Invalid(w, "id", "id — не ulid из 26 символов")
		return 0, false
	}
	if (in.To.DM == "") == (in.To.Room == "") {
		Invalid(w, "to", "to — ровно одно из dm и room")
		return 0, false
	}
	if in.To.DM != "" {
		if !validNick(in.To.DM) {
			Invalid(w, "to", "ник: 2–32 символа, a–z, 0–9, _")
			return 0, false
		}
		if in.KeyID != dmKeyID {
			Invalid(w, "keyId", `keyId личного чата — "dm"`)
			return 0, false
		}
	} else {
		if !validID(in.To.Room) {
			Invalid(w, "to", "room — не 16 байт base64url")
			return 0, false
		}
		if !validID(in.KeyID) {
			Invalid(w, "keyId", "keyId — не 16 байт base64url")
			return 0, false
		}
	}
	if _, ok := decodeExactly(in.IV, ivLen); !ok {
		Invalid(w, "iv", "iv — не 12 байт base64url")
		return 0, false
	}
	if ct, err := b64.DecodeString(in.CT); err != nil || len(ct) < minCTLen {
		Invalid(w, "ct", "ct — не base64url или слишком короткий")
		return 0, false
	}
	return ms, true
}

// POST /api/ack — клиент записал сообщения в IndexedDB: из очереди
// устройства их можно убрать (ADR-008).
func (s *server) ack(w http.ResponseWriter, r *http.Request) {
	var in struct {
		IDs []string `json:"ids"`
	}
	if !decode(w, r, &in) {
		return
	}
	if len(in.IDs) > maxAck {
		Invalid(w, "ids", "не больше 500 идентификаторов")
		return
	}
	// Устройство — право: после формы тела (ADR-043).
	device, ok := s.device(w, r)
	if !ok {
		return
	}
	if err := s.st.Ack(r.Context(), device, in.IDs); err != nil {
		s.internal(w, r, err)
		return
	}
	noContent(w)
}
