package platform

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
)

// UserInfo is the display identity the WebView store needs (mirrors the web
// app's User shape: id / name / avatar).
type UserInfo struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Avatar string `json:"avatar"`
}

// LoginResult is the full result Go keeps internally: tokens + identity. The
// refresh_token never crosses into the WebView — see SessionView and the
// credential policy in session.go.
type LoginResult struct {
	AccessToken  string   `json:"access_token"`
	RefreshToken string   `json:"refresh_token"`
	ExpiresIn    int      `json:"expires_in"`
	User         UserInfo `json:"user"`
}

// SessionView is the refresh-token-free projection handed to the WebView (login
// emit, refresh return, boot injection). The refresh_token — the long-lived
// secret — is deliberately omitted: it lives only in the Go process and the
// on-disk session, never in JS or the injected HTML.
type SessionView struct {
	AccessToken string   `json:"access_token"`
	ExpiresIn   int      `json:"expires_in"`
	User        UserInfo `json:"user"`
}

// View projects a LoginResult to the refresh-free SessionView.
func (r LoginResult) View() SessionView {
	return SessionView{AccessToken: r.AccessToken, ExpiresIn: r.ExpiresIn, User: r.User}
}

// UserFromToken extracts the display identity from OIDC tokens WITHOUT verifying
// the signature. This mirrors the backend's extractUser (internal/httpapi/auth.go)
// exactly: prefer the id_token's richer claims, fall back to the access_token;
// name priority preferred_username → name → email → sub; avatar avatar → picture.
//
// Skipping verification is safe for the same reason it is on the backend: the
// auth middleware verifies the access_token on every API call via JWKS, so any
// tamper surfaces there. These claims only drive local display.
func UserFromToken(idToken, accessToken string) (UserInfo, error) {
	pick := idToken
	if pick == "" {
		pick = accessToken
	}
	claims, err := decodeJWTClaims(pick)
	if err != nil {
		return UserInfo{}, err
	}

	u := UserInfo{ID: claimString(claims, "sub")}
	for _, k := range []string{"preferred_username", "name", "email"} {
		if v := claimString(claims, k); v != "" {
			u.Name = v
			break
		}
	}
	if u.Name == "" {
		u.Name = u.ID
	}
	for _, k := range []string{"avatar", "picture"} {
		if v := claimString(claims, k); v != "" {
			u.Avatar = v
			break
		}
	}
	return u, nil
}

// decodeJWTClaims base64url-decodes the JWT payload segment and unmarshals it.
// No signature check (see UserFromToken).
func decodeJWTClaims(token string) (map[string]any, error) {
	parts := strings.Split(token, ".")
	if len(parts) < 2 {
		return nil, errors.New("oauth: token is not a JWT")
	}
	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return nil, fmt.Errorf("oauth: decode jwt payload: %w", err)
	}
	var claims map[string]any
	if err := json.Unmarshal(payload, &claims); err != nil {
		return nil, fmt.Errorf("oauth: parse jwt claims: %w", err)
	}
	return claims, nil
}

func claimString(m map[string]any, key string) string {
	if v, ok := m[key].(string); ok {
		return v
	}
	return ""
}
