package api

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"

	"github.com/xmatic-squad/bare/internal/config"
)

// Сервер не умеет и не пытается проверять шифротексты. Он проверяет форму:
// base64url, длины, версии (docs/crypto.md, «Что сервер проверяет»).
const (
	authKeyLen = 32      // байт
	idLen      = 16      // байт: deviceId, keyId, roomId
	ivLen      = 12      // байт
	minCTLen   = 16      // байт: короче тега AES-GCM шифротекста не бывает
	maxBlob    = 8 << 10 // ключевой блоб, docs/protocol.md
)

// b64 — кодировка бинарных полей протокола: base64url без паддинга.
var b64 = base64.RawURLEncoding

// nickRe — ник по ADR-019: только строчные, без регистровых коллизий.
var nickRe = regexp.MustCompile(`^[a-z0-9_]{2,32}$`)

func validNick(nick string) bool { return nickRe.MatchString(nick) }

// decodeExactly разбирает base64url и требует ровно n байт.
func decodeExactly(s string, n int) ([]byte, bool) {
	raw, err := b64.DecodeString(s)
	if err != nil || len(raw) != n {
		return nil, false
	}
	return raw, true
}

// authKey разбирает authKey клиента: base64url ровно 32 байта.
func authKey(s string) ([]byte, bool) { return decodeExactly(s, authKeyLen) }

// validID — deviceId, keyId и roomId устроены одинаково: 16 случайных
// байт base64url, 22 символа (docs/crypto.md, «Идентификаторы»).
func validID(s string) bool {
	_, ok := decodeExactly(s, idLen)
	return ok
}

// jwkPublic — публичный ключ в том виде, в каком сервер его хранит
// и отдаёт: четыре поля и ничего больше.
type jwkPublic struct {
	Kty string `json:"kty"`
	Crv string `json:"crv"`
	X   string `json:"x"`
	Y   string `json:"y"`
}

// publicKeyJSON проверяет JWK и отдаёт его канонический JSON.
//
// Поле d — приватный ключ. Его наличие означает, что клиент собирается
// отдать серверу материал, которого у сервера не должно быть ни при каких
// условиях, поэтому такой запрос отвергается целиком, а не чистится молча.
// Всё, что не kty, crv, x и y, отбрасывается: хранится ровно то, что нужно.
func publicKeyJSON(raw json.RawMessage) (string, error) {
	var in struct {
		Kty string          `json:"kty"`
		Crv string          `json:"crv"`
		X   string          `json:"x"`
		Y   string          `json:"y"`
		D   json.RawMessage `json:"d"`
	}
	if len(raw) == 0 {
		return "", errors.New("нет публичного ключа")
	}
	if err := json.Unmarshal(raw, &in); err != nil {
		return "", errors.New("публичный ключ — не jwk")
	}
	if in.D != nil {
		return "", errors.New("приватному ключу на сервере не место")
	}
	if in.Kty != "EC" || in.Crv != "P-256" {
		return "", errors.New("ожидается ключ ec p-256")
	}
	if _, ok := decodeExactly(in.X, 32); !ok {
		return "", errors.New("x — не 32 байта base64url")
	}
	if _, ok := decodeExactly(in.Y, 32); !ok {
		return "", errors.New("y — не 32 байта base64url")
	}
	out, err := json.Marshal(jwkPublic{Kty: in.Kty, Crv: in.Crv, X: in.X, Y: in.Y})
	if err != nil {
		return "", err
	}
	return string(out), nil
}

// blobIterations проверяет форму ключевого блоба (docs/crypto.md,
// «Ключевой блоб») и отдаёт iter. Это единственное поле блоба, которое
// сервер читает: его же отдаёт GET /api/kdf. Всё остальное — непрозрачный
// шифротекст.
func blobIterations(blob string) (int, error) {
	if blob == "" {
		return 0, errors.New("нет ключевого блоба")
	}
	if len(blob) > maxBlob {
		return 0, errors.New("ключевой блоб больше 8 КиБ")
	}
	var b struct {
		V    int    `json:"v"`
		Iter int    `json:"iter"`
		IV   string `json:"iv"`
		CT   string `json:"ct"`
	}
	if err := json.Unmarshal([]byte(blob), &b); err != nil {
		return 0, errors.New("ключевой блоб — не json")
	}
	if b.V != 1 {
		return 0, fmt.Errorf("версия блоба %d, ожидается 1", b.V)
	}
	if b.Iter < config.KDFMinIterations || b.Iter > config.KDFMaxIterations {
		return 0, fmt.Errorf("iter вне границ %d…%d", config.KDFMinIterations, config.KDFMaxIterations)
	}
	if _, ok := decodeExactly(b.IV, ivLen); !ok {
		return 0, errors.New("iv — не 12 байт base64url")
	}
	if ct, err := b64.DecodeString(b.CT); err != nil || len(ct) < minCTLen {
		return 0, errors.New("ct — не base64url или слишком короткий")
	}
	return b.Iter, nil
}
