// Copyright 2026 printing-press-golden and contributors. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.

package oauth

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

	"printing-press-oauth2-pp-cli/internal/cliutil"
)

const DeviceCodeGrant = "urn:ietf:params:oauth:grant-type:device_code"

type DeviceCodeResponse struct {
	DeviceCode              string `json:"device_code"`
	UserCode                string `json:"user_code"`
	VerificationURI         string `json:"verification_uri"`
	VerificationURIComplete string `json:"verification_uri_complete"`
	ExpiresIn               int    `json:"expires_in"`
	Interval                int    `json:"interval"`
	Message                 string `json:"message"`
}

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

type tokenError struct {
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description"`
}

func RequestDeviceCode(ctx context.Context, httpClient *http.Client, endpoint, clientID string, scopes []string) (*DeviceCodeResponse, error) {
	if httpClient == nil {
		httpClient = http.DefaultClient
	}
	form := url.Values{"client_id": {clientID}}
	if len(scopes) > 0 {
		form.Set("scope", strings.Join(scopes, " "))
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, fmt.Errorf("building device-code request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("requesting device code: %w", err)
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("requesting device code: HTTP %d: %s", resp.StatusCode, sanitizeOAuthError(string(body), clientID))
	}
	var out DeviceCodeResponse
	if err := json.Unmarshal(body, &out); err != nil {
		return nil, fmt.Errorf("parsing device-code response: %w", err)
	}
	if out.DeviceCode == "" || out.UserCode == "" || out.VerificationURI == "" {
		return nil, fmt.Errorf("device-code response missing device_code, user_code, or verification_uri")
	}
	return &out, nil
}

func PollDeviceToken(ctx context.Context, httpClient *http.Client, tokenURL, clientID, deviceCode string, interval time.Duration) (*TokenResponse, error) {
	if httpClient == nil {
		httpClient = http.DefaultClient
	}
	if interval <= 0 {
		interval = 5 * time.Second
	}
	nextDelay := time.Duration(0)
	for {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(nextDelay):
		}

		tok, retry, err := pollDeviceTokenOnce(ctx, httpClient, tokenURL, clientID, deviceCode)
		if err == nil {
			return tok, nil
		}
		switch retry {
		case "authorization_pending":
			nextDelay = interval
		case "slow_down":
			interval += 5 * time.Second
			nextDelay = interval
		default:
			return nil, err
		}
	}
}

func pollDeviceTokenOnce(ctx context.Context, httpClient *http.Client, tokenURL, clientID, deviceCode string) (*TokenResponse, string, error) {
	form := url.Values{
		"grant_type":  {DeviceCodeGrant},
		"device_code": {deviceCode},
		"client_id":   {clientID},
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, "", fmt.Errorf("building token poll request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, "", fmt.Errorf("polling token endpoint: %w", err)
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode == http.StatusOK {
		var tok TokenResponse
		if err := json.Unmarshal(body, &tok); err != nil {
			return nil, "", fmt.Errorf("parsing token response: %w", err)
		}
		if tok.AccessToken == "" {
			return nil, "", fmt.Errorf("token response missing access_token")
		}
		return &tok, "", nil
	}
	var terr tokenError
	_ = json.Unmarshal(body, &terr)
	if terr.Error == "" {
		return nil, "", fmt.Errorf("token endpoint returned HTTP %d: %s", resp.StatusCode, sanitizeOAuthError(string(body), clientID, deviceCode))
	}
	if terr.Error == "authorization_pending" || terr.Error == "slow_down" {
		return nil, terr.Error, errors.New(terr.Error)
	}
	if terr.ErrorDescription != "" {
		return nil, "", fmt.Errorf("%s: %s", sanitizeOAuthError(terr.Error, clientID, deviceCode), sanitizeOAuthError(terr.ErrorDescription, clientID, deviceCode))
	}
	return nil, "", errors.New(sanitizeOAuthError(terr.Error, clientID, deviceCode))
}

func RefreshToken(ctx context.Context, httpClient *http.Client, tokenURL, clientID, clientSecret, refreshToken string) (*TokenResponse, error) {
	if httpClient == nil {
		httpClient = http.DefaultClient
	}
	form := url.Values{
		"grant_type":    {"refresh_token"},
		"refresh_token": {refreshToken},
		"client_id":     {clientID},
	}
	if clientSecret != "" {
		form.Set("client_secret", clientSecret)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, fmt.Errorf("building refresh request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("refreshing access token: %w", err)
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("refreshing access token: HTTP %d: %s", resp.StatusCode, sanitizeOAuthError(string(body), clientID, clientSecret, refreshToken))
	}
	var tok TokenResponse
	if err := json.Unmarshal(body, &tok); err != nil {
		return nil, fmt.Errorf("parsing refresh response: %w", err)
	}
	if tok.AccessToken == "" {
		return nil, fmt.Errorf("refresh response missing access_token")
	}
	return &tok, nil
}

func sanitizeOAuthError(msg string, secrets ...string) string {
	for _, secret := range secrets {
		secret = strings.TrimSpace(secret)
		if secret != "" {
			msg = strings.ReplaceAll(msg, secret, "[REDACTED]")
		}
	}
	return cliutil.SanitizeErrorBody(msg)
}
