package auth

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

const (
	// SessionCookieName + RefreshCookieName are the canonical cookie names
	// issued after the patholo → paliad rename (2026-04-16). The legacy
	// patholo_* fallback was removed in t-paliad-081 (2026-04-30) — any
	// user who held a legacy cookie has long since been re-authed through
	// the upgrade path and now carries paliad_* names.
	SessionCookieName = "paliad_session"
	RefreshCookieName = "paliad_refresh"

	CookieMaxAge = 30 * 24 * 60 * 60 // 30 days
)

type Client struct {
	URL       string
	AnonKey   string
	JWTSecret []byte
	HTTP      *http.Client
}

func NewClient(supabaseURL, anonKey string, jwtSecret []byte) *Client {
	if len(jwtSecret) == 0 {
		log.Fatal("SUPABASE_JWT_SECRET must be set — session cookies cannot be trusted without signature verification")
	}
	return &Client{
		URL:       strings.TrimRight(supabaseURL, "/"),
		AnonKey:   anonKey,
		JWTSecret: jwtSecret,
		HTTP:      &http.Client{Timeout: 10 * time.Second},
	}
}

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	ExpiresIn    int    `json:"expires_in"`
}

// SignIn authenticates a user with email and password.
func (c *Client) SignIn(email, password string) (*TokenResponse, error) {
	return c.tokenRequest("password", map[string]string{
		"email":    email,
		"password": password,
	})
}

// RefreshSession exchanges a refresh token for a new access token.
func (c *Client) RefreshSession(refreshToken string) (*TokenResponse, error) {
	return c.tokenRequest("refresh_token", map[string]string{
		"refresh_token": refreshToken,
	})
}

func (c *Client) tokenRequest(grantType string, body map[string]string) (*TokenResponse, error) {
	jsonBody, err := json.Marshal(body)
	if err != nil {
		return nil, fmt.Errorf("marshal: %w", err)
	}

	endpoint := fmt.Sprintf("%s/auth/v1/token?grant_type=%s", c.URL, grantType)
	req, err := http.NewRequest("POST", endpoint, bytes.NewReader(jsonBody))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("apikey", c.AnonKey)

	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("status %d: %s", resp.StatusCode, parseErrorMessage(respBody))
	}

	var result TokenResponse
	if err := json.Unmarshal(respBody, &result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}
	if result.AccessToken == "" {
		return nil, errors.New("empty access_token")
	}
	return &result, nil
}

// SignUp registers a new user. Returns tokens if auto-confirm is enabled, nil otherwise.
func (c *Client) SignUp(email, password string) (*TokenResponse, error) {
	jsonBody, err := json.Marshal(map[string]string{
		"email":    email,
		"password": password,
	})
	if err != nil {
		return nil, fmt.Errorf("marshal: %w", err)
	}

	endpoint := fmt.Sprintf("%s/auth/v1/signup", c.URL)
	req, err := http.NewRequest("POST", endpoint, bytes.NewReader(jsonBody))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("apikey", c.AnonKey)

	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("status %d: %s", resp.StatusCode, parseErrorMessage(respBody))
	}

	var result TokenResponse
	if err := json.Unmarshal(respBody, &result); err != nil || result.AccessToken == "" {
		return nil, nil // registered but no auto-login (email confirmation pending)
	}
	return &result, nil
}

// SignOut invalidates the user's session on Supabase.
func (c *Client) SignOut(accessToken string) {
	endpoint := fmt.Sprintf("%s/auth/v1/logout", c.URL)
	req, err := http.NewRequest("POST", endpoint, nil)
	if err != nil {
		return
	}
	req.Header.Set("apikey", c.AnonKey)
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := c.HTTP.Do(req)
	if err != nil {
		return
	}
	resp.Body.Close()
}

// VerifiedClaims is the subset of Supabase JWT claims the app cares about.
// Populated only after signature + expiry verification succeed.
type VerifiedClaims struct {
	Sub   string
	Email string
	Exp   time.Time
}

// VerifyToken parses and fully validates a Supabase access token. It verifies
// the HS256 signature against the shared secret, enforces the standard
// exp/nbf/iat checks, and extracts the `sub` claim as the authenticated
// user's UUID string. Returns an error if the signature is wrong, the token
// is expired, or any required claim is missing.
func (c *Client) VerifyToken(token string) (*VerifiedClaims, error) {
	parsed, err := jwt.Parse(token, func(t *jwt.Token) (any, error) {
		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
		}
		return c.JWTSecret, nil
	}, jwt.WithValidMethods([]string{"HS256"}))
	if err != nil {
		return nil, err
	}
	claims, ok := parsed.Claims.(jwt.MapClaims)
	if !ok || !parsed.Valid {
		return nil, errors.New("invalid claims")
	}
	sub, _ := claims["sub"].(string)
	if sub == "" {
		return nil, errors.New("no sub claim")
	}
	email, _ := claims["email"].(string)
	var exp time.Time
	if expClaim, err := claims.GetExpirationTime(); err == nil && expClaim != nil {
		exp = expClaim.Time
	}
	return &VerifiedClaims{Sub: sub, Email: email, Exp: exp}, nil
}

// readSessionCookie returns the value of the session cookie, or "" if the
// caller did not present one.
func readSessionCookie(r *http.Request) string {
	if c, err := r.Cookie(SessionCookieName); err == nil && c.Value != "" {
		return c.Value
	}
	return ""
}

// readRefreshCookie mirrors readSessionCookie for the refresh token.
func readRefreshCookie(r *http.Request) string {
	if c, err := r.Cookie(RefreshCookieName); err == nil && c.Value != "" {
		return c.Value
	}
	return ""
}

// Middleware requires a valid, signature-verified session for protected routes.
// Browser requests get a 302 to /login; API requests get a 401 JSON response.
func (c *Client) Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		sessionValue := readSessionCookie(r)
		if sessionValue == "" {
			rejectUnauthenticated(w, r)
			return
		}

		claims, err := c.VerifyToken(sessionValue)
		if err == nil {
			ctx := withVerifiedClaims(r.Context(), claims)
			next.ServeHTTP(w, r.WithContext(ctx))
			return
		}

		// Only attempt refresh if the failure was expiry. Any other signature
		// error means the token is forged or tampered with — reject.
		if !errors.Is(err, jwt.ErrTokenExpired) {
			ClearAuthCookies(w)
			rejectUnauthenticated(w, r)
			return
		}

		refreshValue := readRefreshCookie(r)
		if refreshValue == "" {
			ClearAuthCookies(w)
			rejectUnauthenticated(w, r)
			return
		}

		tokens, err := c.RefreshSession(refreshValue)
		if err != nil {
			log.Printf("token refresh failed: %v", err)
			ClearAuthCookies(w)
			rejectUnauthenticated(w, r)
			return
		}

		newClaims, err := c.VerifyToken(tokens.AccessToken)
		if err != nil {
			log.Printf("refreshed token failed verification: %v", err)
			ClearAuthCookies(w)
			rejectUnauthenticated(w, r)
			return
		}

		SetAuthCookies(w, r, tokens)
		ctx := withVerifiedClaims(r.Context(), newClaims)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func rejectUnauthenticated(w http.ResponseWriter, r *http.Request) {
	if isAPIRequest(r) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusUnauthorized)
		_, _ = w.Write([]byte(`{"error":"unauthenticated"}`))
		return
	}
	http.Redirect(w, r, "/login", http.StatusFound)
}

func isAPIRequest(r *http.Request) bool {
	return strings.HasPrefix(r.URL.Path, "/api/")
}

// SetAuthCookies writes session and refresh token cookies.
func SetAuthCookies(w http.ResponseWriter, r *http.Request, tokens *TokenResponse) {
	secure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
	for _, c := range []*http.Cookie{
		{Name: SessionCookieName, Value: tokens.AccessToken},
		{Name: RefreshCookieName, Value: tokens.RefreshToken},
	} {
		http.SetCookie(w, &http.Cookie{
			Name:     c.Name,
			Value:    c.Value,
			Path:     "/",
			MaxAge:   CookieMaxAge,
			HttpOnly: true,
			SameSite: http.SameSiteLaxMode,
			Secure:   secure,
		})
	}
}

// ClearAuthCookies removes session and refresh token cookies.
func ClearAuthCookies(w http.ResponseWriter) {
	for _, name := range []string{SessionCookieName, RefreshCookieName} {
		http.SetCookie(w, &http.Cookie{
			Name:     name,
			Value:    "",
			Path:     "/",
			MaxAge:   -1,
			HttpOnly: true,
		})
	}
}

func parseErrorMessage(body []byte) string {
	var resp struct {
		Error            string `json:"error"`
		ErrorDescription string `json:"error_description"`
		Message          string `json:"message"`
		Msg              string `json:"msg"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		return string(body)
	}
	for _, msg := range []string{resp.ErrorDescription, resp.Message, resp.Msg, resp.Error} {
		if msg != "" {
			return msg
		}
	}
	return string(body)
}
