package api

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strings"
	"time"

	"github.com/alexedwards/argon2id"
	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"

	"forum-backend/internal/auth"
)

type userProfile struct {
	ID        uuid.UUID `json:"id"`
	Username  string    `json:"username"`
	PostCount int32     `json:"post_count"`
	CreatedAt time.Time `json:"created_at"`
}

type authResponse struct {
	Token    string      `json:"token"`
	User     userProfile `json:"user"`
	ServedBy string      `json:"served_by"`
}

type userResponse struct {
	ID        uuid.UUID `json:"id"`
	Username  string    `json:"username"`
	Email     *string   `json:"email,omitempty"`
	PostCount int32     `json:"post_count"`
	CreatedAt time.Time `json:"created_at"`
	ServedBy  string    `json:"served_by"`
}

// issueSession is the single place that mints a new access JWT + fingerprint
// cookie + refresh cookie and inserts the refresh row in the DB. Called from
// register, login, and refresh — keeps the three flows in lockstep, so a
// security-relevant change (e.g. cookie flags) only has to happen here.
func (app *App) issueSession(ctx context.Context, w http.ResponseWriter, userID uuid.UUID, username string) (string, error) {
	fp, err := auth.NewFingerprint()
	if err != nil {
		return "", err
	}

	rt, err := auth.NewRefreshToken()
	if err != nil {
		return "", err
	}

	_, err = app.db.Exec(ctx,
		`INSERT INTO refresh_tokens (user_id, token_hash, expires_at)
		 VALUES ($1, $2, $3)`,
		(*[16]byte)(&userID), rt.Hash, time.Now().Add(auth.RefreshTokenLifetime),
	)
	if err != nil {
		return "", err
	}

	jwt, err := auth.EncodeJWT(userID, username, fp.Hash, app.config.JWTSecret, app.config.JWTIssuer)
	if err != nil {
		return "", err
	}

	auth.SetFingerprintCookie(w, fp)
	auth.SetRefreshCookie(w, rt)
	return jwt, nil
}

func (app *App) register(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Username string `json:"username"`
		Email    string `json:"email"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, unprocessable("invalid JSON"))
		return
	}
	req.Username = strings.TrimSpace(req.Username)
	req.Email = strings.ToLower(strings.TrimSpace(req.Email))

	if req.Username == "" || len(req.Password) < 12 {
		writeError(w, unprocessable("username required and password must be >= 12 chars"))
		return
	}

	hash, err := argon2id.CreateHash(req.Password, argon2id.DefaultParams)
	if err != nil {
		writeError(w, internal(err.Error()))
		return
	}

	var (
		id        uuid.UUID
		username  string
		postCount int32
		createdAt time.Time
	)
	err = app.db.QueryRow(r.Context(),
		`INSERT INTO users (username, email, password_hash)
		 VALUES ($1, $2, $3)
		 RETURNING id, username, post_count, created_at`,
		req.Username, req.Email, hash,
	).Scan((*[16]byte)(&id), &username, &postCount, &createdAt)
	if err != nil {
		writeError(w, pgxToAppError(err))
		return
	}

	token, err := app.issueSession(r.Context(), w, id, username)
	if err != nil {
		writeError(w, internal(err.Error()))
		return
	}

	writeJSON(w, http.StatusCreated, authResponse{
		Token:    token,
		User:     userProfile{ID: id, Username: username, PostCount: postCount, CreatedAt: createdAt},
		ServedBy: app.config.InstanceID,
	})
}

func (app *App) login(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, unprocessable("invalid JSON"))
		return
	}

	var (
		id           uuid.UUID
		username     string
		passwordHash string
		postCount    int32
		createdAt    time.Time
	)
	err := app.db.QueryRow(r.Context(),
		`SELECT id, username, password_hash, post_count, created_at
		 FROM users WHERE username = $1`,
		strings.TrimSpace(req.Username),
	).Scan((*[16]byte)(&id), &username, &passwordHash, &postCount, &createdAt)
	if err != nil {
		writeError(w, errUnauthorized)
		return
	}

	match, err := argon2id.ComparePasswordAndHash(req.Password, passwordHash)
	if err != nil || !match {
		writeError(w, errUnauthorized)
		return
	}

	token, err := app.issueSession(r.Context(), w, id, username)
	if err != nil {
		writeError(w, internal(err.Error()))
		return
	}

	writeJSON(w, http.StatusOK, authResponse{
		Token:    token,
		User:     userProfile{ID: id, Username: username, PostCount: postCount, CreatedAt: createdAt},
		ServedBy: app.config.InstanceID,
	})
}

// refresh exchanges a valid __Secure-Refresh cookie for a new access token.
// Rotation: the presented refresh token is marked revoked and a brand-new one
// is issued in the same response. The fingerprint cookie is also rotated so the
// new access token is bound to a fresh user context (OWASP recommendation).
func (app *App) refresh(w http.ResponseWriter, r *http.Request) {
	cookie, err := r.Cookie(auth.RefreshCookieName)
	if err != nil || cookie.Value == "" {
		auth.ClearFingerprintCookie(w)
		auth.ClearRefreshCookie(w)
		writeError(w, errUnauthorized)
		return
	}

	hash := auth.HashRefreshToken(cookie.Value)

	// Atomically validate AND revoke in a single statement to avoid race
	// conditions where two concurrent /refresh calls would both accept the same
	// token. RETURNING gives us the user_id we need to mint the next session.
	var userID uuid.UUID
	err = app.db.QueryRow(r.Context(),
		`UPDATE refresh_tokens
		 SET revoked_at = NOW()
		 WHERE token_hash = $1
		   AND revoked_at IS NULL
		   AND expires_at > NOW()
		 RETURNING user_id`,
		hash,
	).Scan((*[16]byte)(&userID))
	if err != nil {
		// pgx.ErrNoRows = token unknown / already used / expired
		auth.ClearFingerprintCookie(w)
		auth.ClearRefreshCookie(w)
		if errors.Is(err, pgx.ErrNoRows) {
			writeError(w, errUnauthorized)
			return
		}
		writeError(w, internal(err.Error()))
		return
	}

	var username string
	if err := app.db.QueryRow(r.Context(),
		`SELECT username FROM users WHERE id = $1`,
		(*[16]byte)(&userID),
	).Scan(&username); err != nil {
		writeError(w, pgxToAppError(err))
		return
	}

	token, err := app.issueSession(r.Context(), w, userID, username)
	if err != nil {
		writeError(w, internal(err.Error()))
		return
	}

	writeJSON(w, http.StatusOK, map[string]string{
		"token":     token,
		"served_by": app.config.InstanceID,
	})
}

func (app *App) me(w http.ResponseWriter, r *http.Request) {
	u, _ := auth.UserFrom(r)

	var (
		id        uuid.UUID
		username  string
		email     string
		postCount int32
		createdAt time.Time
	)
	err := app.db.QueryRow(r.Context(),
		`SELECT id, username, email, post_count, created_at
		 FROM users WHERE id = $1`,
		(*[16]byte)(&u.UserID),
	).Scan((*[16]byte)(&id), &username, &email, &postCount, &createdAt)
	if err != nil {
		writeError(w, pgxToAppError(err))
		return
	}

	writeJSON(w, http.StatusOK, userResponse{
		ID:        id,
		Username:  username,
		Email:     &email,
		PostCount: postCount,
		CreatedAt: createdAt,
		ServedBy:  app.config.InstanceID,
	})
}

func (app *App) getUser(w http.ResponseWriter, r *http.Request) {
	userID, err := uuid.Parse(chi.URLParam(r, "userID"))
	if err != nil {
		writeError(w, errNotFound)
		return
	}

	var (
		id        uuid.UUID
		username  string
		postCount int32
		createdAt time.Time
	)
	err = app.db.QueryRow(r.Context(),
		`SELECT id, username, post_count, created_at
		 FROM users WHERE id = $1`,
		(*[16]byte)(&userID),
	).Scan((*[16]byte)(&id), &username, &postCount, &createdAt)
	if err != nil {
		writeError(w, pgxToAppError(err))
		return
	}

	writeJSON(w, http.StatusOK, userResponse{
		ID:        id,
		Username:  username,
		PostCount: postCount,
		CreatedAt: createdAt,
		ServedBy:  app.config.InstanceID,
	})
}

// logout clears both the fingerprint and refresh cookies, and revokes the
// presented refresh token in the DB so it cannot be reused even if intercepted.
// The endpoint is public so a client whose access token has already expired
// can still tidy up its session.
func (app *App) logout(w http.ResponseWriter, r *http.Request) {
	if cookie, err := r.Cookie(auth.RefreshCookieName); err == nil && cookie.Value != "" {
		// Best-effort revoke. We don't care if the row doesn't exist (token
		// was forged or already revoked); the cookie clear below is enough.
		_, _ = app.db.Exec(r.Context(),
			`UPDATE refresh_tokens SET revoked_at = NOW()
			 WHERE token_hash = $1 AND revoked_at IS NULL`,
			auth.HashRefreshToken(cookie.Value),
		)
	}

	auth.ClearFingerprintCookie(w)
	auth.ClearRefreshCookie(w)
	writeJSON(w, http.StatusOK, map[string]string{"served_by": app.config.InstanceID})
}
