// Package plicflix troca o token do plicflix pelo jogador, no SEU servidor Go.
// Só biblioteca padrão: copie este arquivo para o seu projeto (docs/SDK.md).
//
//	p := plicflix.Novo(os.Getenv("PLICFLIX_CHAVE"), "")   // "" = API oficial
//	r, err := p.Trocar(ctx, token)                         // r.Jogador.ID, r.Jogador.Nome
//
// O token vale 5 minutos ou UMA troca.
//
// "Entrar com Plicflix" fora do app (OAuth 2.0 + PKCE — /docs/OAUTH):
//
//	pk := plicflix.NovoPKCE()                                  // guarde pk.Verificador e pk.Estado num cookie curto
//	http.Redirect(w, r, plicflix.URLDeEntrada("meu-jogo", volta, pk), http.StatusFound)
//	// na volta: confira r.URL.Query().Get("state") == estado e
//	e, err := p.TrocarCodigo(ctx, "meu-jogo", r.URL.Query().Get("code"), volta, verificador)  // e.Jogador.ID
package plicflix

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

// APIPadrao é o endereço da API do plicflix.
const APIPadrao = "https://api.plicflix.com"

// ErrToken: o token não vale (inválido, vencido ou já trocado).
var ErrToken = errors.New("plicflix: token inválido, vencido ou já usado")

// Troca é a resposta: tudo o que o plicflix entrega ao seu jogo.
type Troca struct {
	Jogador struct {
		ID   string `json:"id"` // "pf_…": fixo para esse jogador no seu jogo
		Nome string `json:"nome"`
	} `json:"jogador"`
	Jogo      string `json:"jogo"`
	Partida   string `json:"partida"`
	Assinante bool   `json:"assinante"`
	Contexto  struct {
		Ref        string `json:"ref"`
		Idioma     string `json:"idioma"`
		Plataforma string `json:"plataforma"`
	} `json:"contexto"`
}

// Cliente da troca.
type Cliente struct {
	chave, api string
	http       *http.Client
}

// Novo cliente com a chave do parceiro (pk_…).
func Novo(chave, api string) *Cliente {
	if api == "" {
		api = APIPadrao
	}
	return &Cliente{chave: chave, api: strings.TrimRight(api, "/"), http: &http.Client{Timeout: 8 * time.Second}}
}

// DoAmbiente: o cliente com PLICFLIX_CHAVE e PLICFLIX_API (vazio: a API de produção).
func DoAmbiente() *Cliente { return Novo(os.Getenv("PLICFLIX_CHAVE"), os.Getenv("PLICFLIX_API")) }

// Trocar o token pelo jogador.
func (c *Cliente) Trocar(ctx context.Context, token string) (Troca, error) {
	var t Troca
	if !strings.HasPrefix(token, "pft_") {
		return t, ErrToken
	}
	corpo, _ := json.Marshal(map[string]string{"token": token})
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.api+"/v1/ingresso/trocar", bytes.NewReader(corpo))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+c.chave)
	resp, err := c.http.Do(req)
	if err != nil {
		return t, err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized {
		return t, ErrToken
	}
	if resp.StatusCode != http.StatusOK {
		return t, fmt.Errorf("plicflix: troca respondeu %d", resp.StatusCode)
	}
	return t, json.NewDecoder(resp.Body).Decode(&t)
}

// ---------------------------------------------------------------- "Entrar com Plicflix" (OAuth 2.0 + PKCE)

// AppPadrao: onde fica a página de autorizar.
const AppPadrao = "https://plicflix.com"

// ErrCodigo: o código da volta não vale (inválido, vencido, usado, verificador ou volta errados).
var ErrCodigo = errors.New("plicflix: código inválido, vencido ou já usado")

// PKCE: o par S256 e o state.
type PKCE struct{ Verificador, Desafio, Estado string }

func aleatorio(n int) string {
	b := make([]byte, n)
	_, _ = rand.Read(b)
	return base64.RawURLEncoding.EncodeToString(b)
}

// NovoPKCE: um par novo.
func NovoPKCE() PKCE {
	v := aleatorio(48)
	h := sha256.Sum256([]byte(v))
	return PKCE{Verificador: v, Desafio: base64.RawURLEncoding.EncodeToString(h[:]), Estado: aleatorio(16)}
}

// URLDeEntrada: para onde mandar o navegador (cliente = o slug do jogo).
func URLDeEntrada(cliente, volta string, p PKCE) string {
	app := os.Getenv("PLICFLIX_APP")
	if app == "" {
		app = AppPadrao
	}
	q := url.Values{"response_type": {"code"}, "client_id": {cliente}, "redirect_uri": {volta}, "state": {p.Estado},
		"code_challenge": {p.Desafio}, "code_challenge_method": {"S256"}, "scope": {"perfil"}}
	return strings.TrimRight(app, "/") + "/oauth/authorize?" + q.Encode()
}

// Entrada: a resposta do /v1/oauth/token.
type Entrada struct {
	Jogador struct {
		ID   string `json:"id"` // o mesmo "pf_…" da entrada pelo app
		Nome string `json:"nome"`
	} `json:"jogador"`
	Jogo        string `json:"jogo"`
	Assinante   bool   `json:"assinante"`
	AccessToken string `json:"access_token"`
	ExpiresIn   int    `json:"expires_in"`
}

// TrocarCodigo: o código da volta pelo jogador (com a chave do estúdio).
func (c *Cliente) TrocarCodigo(ctx context.Context, cliente, codigo, volta, verificador string) (Entrada, error) {
	var e Entrada
	if !strings.HasPrefix(codigo, "pfc_") {
		return e, ErrCodigo
	}
	corpo := url.Values{"grant_type": {"authorization_code"}, "code": {codigo}, "redirect_uri": {volta}, "client_id": {cliente}, "code_verifier": {verificador}}
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.api+"/v1/oauth/token", strings.NewReader(corpo.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	if c.chave != "" {
		req.Header.Set("Authorization", "Bearer "+c.chave)
	}
	resp, err := c.http.Do(req)
	if err != nil {
		return e, err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusBadRequest {
		return e, ErrCodigo
	}
	if resp.StatusCode != http.StatusOK {
		return e, fmt.Errorf("plicflix: token respondeu %d", resp.StatusCode)
	}
	return e, json.NewDecoder(resp.Body).Decode(&e)
}
