package api

import (
	"crypto/subtle"
	"encoding/json"
	"errors"
	"net/http"
	"time"

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

// GET /api/config — то, что клиенту нужно знать до входа.
//
// version и commitAt отвечают на вопрос «какой код сейчас работает»:
// короткая ревизия сборки и время коммита, а не момент компиляции
// (ADR-074). Отдельного эндпоинта им не заводится — конфигурацию клиент
// читает до входа и так.
func (s *server) config(w http.ResponseWriter, r *http.Request) {
	info := build.Current()
	writeJSON(w, http.StatusOK, struct {
		InviteRequired  bool   `json:"inviteRequired"`
		VAPIDPublicKey  string `json:"vapidPublicKey"`
		KDFIterations   int    `json:"kdfIterations"`
		MaxMessageChars int    `json:"maxMessageChars"`
		Version         string `json:"version"`
		CommitAt        int64  `json:"commitAt"`
	}{
		InviteRequired:  s.cfg.InviteCode != "",
		VAPIDPublicKey:  s.cfg.VAPIDPublic,
		KDFIterations:   config.KDFIterations,
		MaxMessageChars: config.MaxMessageChars,
		Version:         info.Version(),
		CommitAt:        info.CommitMilli(),
	})
}

// GET /api/kdf?nick= — сколько итераций PBKDF2 брать для этого ника.
//
// Значение лежит открытым полем iter в ключевом блобе: другого места
// у него нет (docs/crypto.md). Неизвестный ник получает целевое значение
// тем же статусом 200. Скрытием существования ника этот ответ
// не занимается: после повышения цели у аккаунта, который с тех пор
// не входил, iter свой, и по числу его видно (ADR-062). Существование
// ника публично и так (ADR-019).
func (s *server) kdf(w http.ResponseWriter, r *http.Request) {
	iterations := config.KDFIterations
	if nick := r.URL.Query().Get("nick"); validNick(nick) {
		u, err := s.st.User(r.Context(), nick)
		switch {
		case err == nil:
			if iter, err := blobIterations(u.KeyBlob); err == nil {
				iterations = iter
			}
		case errors.Is(err, store.ErrNotFound):
			// молча: целевое значение
		default:
			s.internal(w, r, err)
			return
		}
	}
	writeJSON(w, http.StatusOK, struct {
		Iterations int `json:"iterations"`
	}{iterations})
}

// POST /api/register — регистрация. Сервер проверяет только форму:
// содержимое блоба и стойкость пароля ему недоступны by design.
func (s *server) register(w http.ResponseWriter, r *http.Request) {
	var in struct {
		Nick      string          `json:"nick"`
		AuthKey   string          `json:"authKey"`
		PublicKey json.RawMessage `json:"publicKey"`
		Blob      string          `json:"blob"`
		Invite    string          `json:"invite"`
	}
	if !decode(w, r, &in) {
		return
	}
	// Форма — раньше инвайт-кода: он даёт право регистрироваться, а права
	// идут после формы (ADR-043). Занятость ника этим не выдаётся: nick_taken
	// живёт дальше по тексту, за инвайтом.
	if !validNick(in.Nick) {
		Error(w, http.StatusBadRequest, "invalid_nick", "ник: 2–32 символа, a–z, 0–9, _")
		return
	}
	key, ok := authKey(in.AuthKey)
	if !ok {
		Invalid(w, "authKey", "authKey — не 32 байта base64url")
		return
	}
	public, err := publicKeyJSON(in.PublicKey)
	if err != nil {
		Invalid(w, "publicKey", err.Error())
		return
	}
	if _, err := blobIterations(in.Blob); err != nil {
		Invalid(w, "blob", err.Error())
		return
	}
	// Лимит — 5 в час на IP (ADR-021) — стоит раньше проверки инвайт-кода:
	// иначе код подбирался бы запросами без счёта.
	if wait, ok := s.regs.take(clientIP(r), time.Now()); !ok {
		s.rateLimited(w, wait)
		return
	}
	if code := s.cfg.InviteCode; code != "" {
		if in.Invite == "" {
			Error(w, http.StatusForbidden, "invite_required", "нужен инвайт-код")
			return
		}
		if subtle.ConstantTimeCompare([]byte(code), []byte(in.Invite)) != 1 {
			Error(w, http.StatusForbidden, "invalid_invite", "инвайт-код не подходит")
			return
		}
	}

	cred, err := auth.Hash(key)
	if err != nil {
		s.internal(w, r, err)
		return
	}
	err = s.st.CreateUser(r.Context(), store.User{
		Nick:      in.Nick,
		Cred:      cred,
		PublicKey: public,
		KeyBlob:   in.Blob,
		CreatedAt: time.Now().UnixMilli(),
	})
	if errors.Is(err, store.ErrNickTaken) {
		Error(w, http.StatusConflict, "nick_taken", "ник занят")
		return
	}
	if err != nil {
		s.internal(w, r, err)
		return
	}
	if err := s.startSession(w, r, in.Nick); err != nil {
		s.internal(w, r, err)
		return
	}
	writeJSON(w, http.StatusCreated, struct {
		Nick string `json:"nick"`
	}{in.Nick})
}

// POST /api/login — вход. Ошибка одна на все случаи: неверный ник,
// неверный authKey и кривая форма неразличимы снаружи.
func (s *server) login(w http.ResponseWriter, r *http.Request) {
	var in struct {
		Nick    string `json:"nick"`
		AuthKey string `json:"authKey"`
	}
	if !decode(w, r, &in) {
		return
	}
	key, ok := authKey(in.AuthKey)
	if !ok || !validNick(in.Nick) {
		invalidCredentials(w)
		return
	}
	// Лимит — 10 за 10 минут на пару IP+ник (ADR-021) — стоит раньше
	// хранилища и argon2: перебор не должен заказывать серверу работу.
	// Ключ ведра собирается из адреса и ника через байт, которого нет
	// ни в том ни в другом.
	if wait, ok := s.logins.take(clientIP(r)+"\x00"+in.Nick, time.Now()); !ok {
		s.rateLimited(w, wait)
		return
	}
	u, err := s.st.User(r.Context(), in.Nick)
	if errors.Is(err, store.ErrNotFound) {
		// Считаем впустую: вход с несуществующим ником не должен
		// отвечать заметно быстрее входа с неверным authKey.
		auth.Waste(key)
		invalidCredentials(w)
		return
	}
	if err != nil {
		s.internal(w, r, err)
		return
	}
	valid, rehash := auth.Verify(key, u.Cred)
	if !valid {
		invalidCredentials(w)
		return
	}
	if rehash {
		// Параметры отстали от текущих (ADR-021). Не удалось перехешировать —
		// не повод отказывать во входе: старый хеш остаётся рабочим.
		if cred, err := auth.Hash(key); err != nil {
			s.report(r, err)
		} else if err := s.st.SetAuth(r.Context(), u.Nick, cred); err != nil {
			s.report(r, err)
		}
	}
	if err := s.startSession(w, r, u.Nick); err != nil {
		s.internal(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, struct {
		Nick      string          `json:"nick"`
		PublicKey json.RawMessage `json:"publicKey"`
		Blob      string          `json:"blob"`
	}{u.Nick, json.RawMessage(u.PublicKey), u.KeyBlob})
}

// GET /api/me — кто вошёл.
func (s *server) me(w http.ResponseWriter, r *http.Request) {
	u, ok := s.self(w, r)
	if !ok {
		return
	}
	writeJSON(w, http.StatusOK, struct {
		Nick      string          `json:"nick"`
		PublicKey json.RawMessage `json:"publicKey"`
		CreatedAt int64           `json:"createdAt"`
	}{u.Nick, json.RawMessage(u.PublicKey), u.CreatedAt})
}

// POST /api/logout — выход на этом устройстве.
func (s *server) logout(w http.ResponseWriter, r *http.Request) {
	sess, _ := auth.From(r)
	if err := s.st.DeleteSession(r.Context(), sess.TokenHash); err != nil {
		s.internal(w, r, err)
		return
	}
	auth.ClearCookie(w)
	noContent(w)
}

// POST /api/password — смена пароля и повышение итераций: одна операция
// (ADR-015). Хеш и блоб меняются в одной транзакции.
func (s *server) password(w http.ResponseWriter, r *http.Request) {
	var in struct {
		AuthKey      string `json:"authKey"`
		NewAuthKey   string `json:"newAuthKey"`
		Blob         string `json:"blob"`
		LogoutOthers bool   `json:"logoutOthers"`
	}
	if !decode(w, r, &in) {
		return
	}
	u, ok := s.self(w, r)
	if !ok {
		return
	}
	if !s.confirm(w, in.AuthKey, u) {
		return
	}
	newKey, ok := authKey(in.NewAuthKey)
	if !ok {
		Invalid(w, "newAuthKey", "newAuthKey — не 32 байта base64url")
		return
	}
	if _, err := blobIterations(in.Blob); err != nil {
		Invalid(w, "blob", err.Error())
		return
	}
	cred, err := auth.Hash(newKey)
	if err != nil {
		s.internal(w, r, err)
		return
	}
	sess, _ := auth.From(r)
	revoked, err := s.st.SetPassword(r.Context(), u.Nick, cred, in.Blob, in.LogoutOthers, sess.TokenHash)
	if err != nil {
		s.internal(w, r, err)
		return
	}
	// Сессия проверяется при подключении к потоку, а не в его цикле,
	// поэтому отозванная продолжала бы получать конверты до обрыва
	// соединения. Отзыв доступа закрывает поток сам — тем же способом,
	// что и удаление устройства (ADR-058).
	for _, device := range revoked {
		s.hub.Close(device)
	}
	noContent(w)
}

// DELETE /api/me — удаление аккаунта, подтверждённое authKey.
func (s *server) deleteMe(w http.ResponseWriter, r *http.Request) {
	var in struct {
		AuthKey string `json:"authKey"`
	}
	if !decode(w, r, &in) {
		return
	}
	u, ok := s.self(w, r)
	if !ok {
		return
	}
	if !s.confirm(w, in.AuthKey, u) {
		return
	}
	// Устройства, сессии, контакты, членство, ключи комнат и очереди уносит
	// каскад; комнаты, где пользователь владелец, меняют владельца или
	// удаляются пустыми (ADR-018) — всё в одной транзакции хранилища.
	changes, err := s.st.DeleteUser(r.Context(), u.Nick)
	if err != nil {
		s.internal(w, r, err)
		return
	}
	// Удаление аккаунта — выход из всех его комнат: оставшимся уходит room
	// с needsRekey, каждому со своим ключом (ADR-041).
	for _, change := range changes {
		s.sendRoom(r, change, "")
	}
	auth.ClearCookie(w)
	noContent(w)
}

// GET /api/users/{nick} — публичный ключ собеседника. Доверие к нему —
// TOFU на клиенте (ADR-016).
func (s *server) user(w http.ResponseWriter, r *http.Request) {
	nick := r.PathValue("nick")
	if !validNick(nick) {
		unknownUser(w)
		return
	}
	u, err := s.st.User(r.Context(), nick)
	if errors.Is(err, store.ErrNotFound) {
		unknownUser(w)
		return
	}
	if err != nil {
		s.internal(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, struct {
		Nick      string          `json:"nick"`
		PublicKey json.RawMessage `json:"publicKey"`
	}{u.Nick, json.RawMessage(u.PublicKey)})
}

// self читает пользователя сессии. Строки нет — сессия недействительна:
// аккаунт удалён на другом устройстве.
func (s *server) self(w http.ResponseWriter, r *http.Request) (store.User, bool) {
	sess, _ := auth.From(r)
	u, err := s.st.User(r.Context(), sess.Nick)
	if errors.Is(err, store.ErrNotFound) {
		Error(w, http.StatusUnauthorized, "unauthenticated", "нужен вход")
		return store.User{}, false
	}
	if err != nil {
		s.internal(w, r, err)
		return store.User{}, false
	}
	return u, true
}

// confirm проверяет authKey — подтверждение опасной операции.
func (s *server) confirm(w http.ResponseWriter, given string, u store.User) bool {
	key, ok := authKey(given)
	if !ok {
		invalidCredentials(w)
		return false
	}
	if valid, _ := auth.Verify(key, u.Cred); !valid {
		invalidCredentials(w)
		return false
	}
	return true
}

// startSession выдаёт сессию и ставит cookie.
func (s *server) startSession(w http.ResponseWriter, r *http.Request, nick string) error {
	token, hash, err := auth.NewToken()
	if err != nil {
		return err
	}
	now := time.Now()
	expires := now.Add(auth.TTL)
	if err := s.st.CreateSession(r.Context(), hash, nick, now.UnixMilli(), expires.UnixMilli()); err != nil {
		return err
	}
	auth.SetCookie(w, token, expires)
	return nil
}

func invalidCredentials(w http.ResponseWriter) {
	Error(w, http.StatusUnauthorized, "invalid_credentials", "неверный ник или пароль")
}

func unknownUser(w http.ResponseWriter) {
	Error(w, http.StatusNotFound, "unknown_user", "такого ника нет")
}
