package infra

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

// oauth2TokenResponse es la respuesta JSON estandar del endpoint token de OAuth2.
type oauth2TokenResponse struct {
	AccessToken      string `json:"access_token"`
	RefreshToken     string `json:"refresh_token"`
	TokenType        string `json:"token_type"`
	ExpiresIn        int64  `json:"expires_in"`
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description"`
}

// oauth2DoTokenRequest hace POST application/x-www-form-urlencoded al TokenURL
// con el body indicado, parsea la respuesta JSON y construye OAuthTokens.
func oauth2DoTokenRequest(tokenURL string, form url.Values) (OAuthTokens, error) {
	var zero OAuthTokens
	req, err := http.NewRequest(http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
	if err != nil {
		return zero, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Accept", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return zero, fmt.Errorf("do request: %w", err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return zero, fmt.Errorf("read body: %w", err)
	}

	var parsed oauth2TokenResponse
	if err := json.Unmarshal(body, &parsed); err != nil {
		return zero, fmt.Errorf("parse json: %w (body=%s)", err, string(body))
	}
	if parsed.Error != "" {
		return zero, fmt.Errorf("oauth provider error: %s: %s", parsed.Error, parsed.ErrorDescription)
	}
	if resp.StatusCode >= 400 {
		return zero, fmt.Errorf("http %d: %s", resp.StatusCode, string(body))
	}
	if parsed.AccessToken == "" {
		return zero, fmt.Errorf("respuesta sin access_token")
	}

	var expiresAt int64
	if parsed.ExpiresIn > 0 {
		expiresAt = time.Now().Unix() + parsed.ExpiresIn
	}

	return OAuthTokens{
		AccessToken:  parsed.AccessToken,
		RefreshToken: parsed.RefreshToken,
		TokenType:    parsed.TokenType,
		ExpiresAt:    expiresAt,
	}, nil
}

// Oauth2Exchange intercambia un authorization code por tokens OAuth2.
// Hace POST al TokenURL con grant_type=authorization_code y las credenciales
// del cliente. Retorna OAuthTokens con AccessToken, RefreshToken y ExpiresAt.
func Oauth2Exchange(config OAuthConfig, code string) (OAuthTokens, error) {
	var zero OAuthTokens
	if code == "" {
		return zero, fmt.Errorf("oauth2_exchange: code vacio")
	}
	if config.TokenURL == "" {
		return zero, fmt.Errorf("oauth2_exchange: token_url vacio")
	}
	form := url.Values{}
	form.Set("grant_type", "authorization_code")
	form.Set("code", code)
	form.Set("client_id", config.ClientID)
	form.Set("client_secret", config.ClientSecret)
	form.Set("redirect_uri", config.RedirectURL)

	tokens, err := oauth2DoTokenRequest(config.TokenURL, form)
	if err != nil {
		return zero, fmt.Errorf("oauth2_exchange: %w", err)
	}
	return tokens, nil
}
