package subsonic

import (
	"context"
	"crypto/md5"
	"crypto/subtle"
	"encoding/hex"
	"errors"
	"net/http"
	"strings"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

// Config controls wire-format concessions the server makes for Subsonic
// clients. At-rest policy (which users have a subsonic_password set) is
// driven per-user; this struct only covers the server-wide opt-ins.
type Config struct {
	AllowPlaintextPassword bool
}

type ctxKey int

const userCtxKey ctxKey = 1

// UserFromContext returns the user attached by Middleware.
func UserFromContext(ctx context.Context) (dbq.User, bool) {
	u, ok := ctx.Value(userCtxKey).(dbq.User)
	return u, ok
}

// Middleware authenticates the request against users.api_token (apiKey) or
// users.subsonic_password (t/s or p). On failure it writes a Subsonic failed
// envelope using the request's f= format; downstream handlers never see an
// unauthenticated request.
func Middleware(pool *pgxpool.Pool, cfg Config) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			user, code, msg := authenticate(r, pool, cfg)
			if code != 0 {
				WriteFail(w, r, code, msg)
				return
			}
			ctx := context.WithValue(r.Context(), userCtxKey, user)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

func authenticate(r *http.Request, pool *pgxpool.Pool, cfg Config) (dbq.User, int, string) {
	q := dbq.New(pool)
	params := r.URL.Query()

	if apiKey := params.Get("apiKey"); apiKey != "" {
		user, err := q.GetUserByAPIToken(r.Context(), apiKey)
		if err != nil {
			if errors.Is(err, pgx.ErrNoRows) {
				return dbq.User{}, ErrWrongCredentials, "Invalid apiKey"
			}
			return dbq.User{}, ErrGeneric, "Auth lookup failed"
		}
		return user, 0, ""
	}

	username := params.Get("u")
	if username == "" {
		return dbq.User{}, ErrMissingParameter, "Missing required parameter: u or apiKey"
	}
	user, err := q.GetUserByUsername(r.Context(), username)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return dbq.User{}, ErrWrongCredentials, "Wrong username or password"
		}
		return dbq.User{}, ErrGeneric, "Auth lookup failed"
	}

	if token, salt := params.Get("t"), params.Get("s"); token != "" && salt != "" {
		if user.SubsonicPassword == nil {
			return dbq.User{}, ErrTokenNotSupported, "Subsonic token auth not configured; use apiKey or set a Subsonic password"
		}
		sum := md5.Sum([]byte(*user.SubsonicPassword + salt))
		expected := hex.EncodeToString(sum[:])
		if subtle.ConstantTimeCompare([]byte(expected), []byte(token)) != 1 {
			return dbq.User{}, ErrWrongCredentials, "Wrong username or password"
		}
		return user, 0, ""
	}

	if password := params.Get("p"); password != "" {
		if !cfg.AllowPlaintextPassword {
			return dbq.User{}, ErrTokenNotSupported, "Plaintext password auth is disabled; use apiKey or t+s"
		}
		if user.SubsonicPassword == nil {
			return dbq.User{}, ErrTokenNotSupported, "Subsonic password not configured for this user"
		}
		decoded, err := decodePassword(password)
		if err != nil {
			return dbq.User{}, ErrWrongCredentials, "Malformed password"
		}
		if subtle.ConstantTimeCompare([]byte(*user.SubsonicPassword), []byte(decoded)) != 1 {
			return dbq.User{}, ErrWrongCredentials, "Wrong username or password"
		}
		return user, 0, ""
	}

	return dbq.User{}, ErrMissingParameter, "Missing required parameter: p, t+s, or apiKey"
}

// decodePassword unwraps Subsonic's enc:HEX obfuscation. Non-enc values pass
// through unchanged so the caller can treat the result as the literal secret.
func decodePassword(p string) (string, error) {
	if !strings.HasPrefix(p, "enc:") {
		return p, nil
	}
	b, err := hex.DecodeString(p[4:])
	if err != nil {
		return "", err
	}
	return string(b), nil
}
