// Exemplo de integração pela API REST: o sistema de uma loja consulta o catálogo, os
// clientes e os últimos pedidos da empresa. Com GRAVAR=1 ele também cadastra um cliente
// de exemplo (ou reaproveita o que já existe com o mesmo CPF) e lança um pedido para
// ele com o primeiro produto do catálogo.
//
// Só a biblioteca padrão do Go — nada para instalar.
//
// Com o usuário da integração:
//
//	API_BASE=https://app.suaempresa.com.br \
//	API_USUARIO=integracao@suaempresa.com.br \
//	API_SENHA='senha do usuário da integração' \
//	go run exemplo.go
//
// Com uma conexão OAuth2, no lugar de usuário e senha: API_TOKEN=<token> API_ORG=<empresa>.
// API_ORG também escolhe a empresa no login quando o usuário tem acesso a mais de uma.
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

// CPF válido reservado para o cliente de exemplo: rodar de novo reaproveita o cadastro.
const cpfDoExemplo = "11144477735"

var (
	base       = strings.TrimRight(os.Getenv("API_BASE"), "/")
	cabecalhos = map[string]string{"Content-Type": "application/json"}
)

type produto struct {
	ID            string  `json:"id"`
	Nome          string  `json:"nome"`
	ValorUnitario float64 `json:"valorUnitario"`
}

type pessoa struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	CpfCnpj string `json:"cpfCnpj"`
}

type pedido struct {
	ID         string  `json:"id"`
	Numero     int     `json:"numero"`
	Situacao   string  `json:"situacao"`
	ValorTotal float64 `json:"valorTotal"`
	Pessoa     struct {
		Nome string `json:"nome"`
	} `json:"pessoa"`
}

func chama(metodo, rota string, corpo, resposta any) {
	var dados io.Reader
	if corpo != nil {
		codificado, err := json.Marshal(corpo)
		confere(err)
		dados = bytes.NewReader(codificado)
	}
	req, err := http.NewRequest(metodo, base+rota, dados)
	confere(err)
	for chave, valor := range cabecalhos {
		req.Header.Set(chave, valor)
	}
	resp, err := http.DefaultClient.Do(req)
	confere(err)
	defer resp.Body.Close()
	texto, err := io.ReadAll(resp.Body)
	confere(err)
	if resp.StatusCode >= 300 {
		// O corpo traz o motivo, ex.: {"code":9,"message":"o acesso por API exige o módulo..."}
		para("%s %s falhou com %d: %s", metodo, rota, resp.StatusCode, texto)
	}
	if resposta != nil {
		confere(json.Unmarshal(texto, resposta))
	}
}

// entra monta a credencial das chamadas e devolve o nome de quem entrou.
func entra() string {
	if token := os.Getenv("API_TOKEN"); token != "" {
		cabecalhos["Authorization"] = "Bearer " + token
		cabecalhos["Org"] = os.Getenv("API_ORG")
		return "conexão OAuth2"
	}
	var login struct {
		Name       string `json:"name"`
		Token      string `json:"token"`
		CurrentOrg struct {
			ID string `json:"id"`
		} `json:"currentOrg"`
	}
	chama("POST", "/api/users/login", map[string]string{
		"username": os.Getenv("API_USUARIO"),
		"password": os.Getenv("API_SENHA"),
		"orgId":    os.Getenv("API_ORG"),
	}, &login)
	cabecalhos["Token"] = login.Token
	cabecalhos["Org"] = login.CurrentOrg.ID
	if org := os.Getenv("API_ORG"); org != "" {
		cabecalhos["Org"] = org
	}
	return login.Name
}

// clienteDeExemplo devolve o cliente do CPF de exemplo, cadastrando-o na primeira vez.
func clienteDeExemplo() pessoa {
	var achados struct {
		PersonList []pessoa `json:"personList"`
	}
	chama("POST", "/api/pessoas/list", map[string]any{"cpfCnpjs": []string{cpfDoExemplo}}, &achados)
	if len(achados.PersonList) > 0 {
		return achados.PersonList[0]
	}
	var criado struct {
		Person pessoa `json:"person"`
	}
	chama("POST", "/api/pessoas", map[string]any{"person": map[string]any{
		"name":    "Cliente de exemplo da integração",
		"cpfCnpj": cpfDoExemplo,
		"tags":    []map[string]string{{"value": "customer"}},
	}}, &criado)
	return criado.Person
}

// lancaPedido abre o pedido e inclui o item. O item vai pela rota de produtos do
// pedido, como na tela: é ela que traz nome, unidade e preço do cadastro e calcula os
// totais.
func lancaPedido(comprador pessoa, item produto) pedido {
	var aberto struct {
		Pedido pedido `json:"pedido"`
	}
	chama("POST", "/api/pedidos", map[string]any{"pedido": map[string]any{
		"tipo":   "ped",
		"pessoa": map[string]string{"id": comprador.ID, "nome": comprador.Name},
		"obs":    "Pedido de exemplo lançado pela API",
	}}, &aberto)
	chama("POST", "/api/pedidos/"+aberto.Pedido.ID+"/produtos", map[string]any{
		"productId": item.ID,
		"product":   map[string]any{"produtoId": item.ID, "quantidade": 2},
	}, nil)
	var lido struct {
		Pedido pedido `json:"pedido"`
	}
	chama("GET", "/api/pedidos/"+aberto.Pedido.ID, nil, &lido)
	return lido.Pedido
}

func main() {
	acesso := os.Getenv("API_TOKEN") != "" || (os.Getenv("API_USUARIO") != "" && os.Getenv("API_SENHA") != "")
	if base == "" || !acesso {
		para("Informe API_BASE e o acesso (API_USUARIO e API_SENHA, ou API_TOKEN e API_ORG).")
	}
	fmt.Printf("Conectado como %s na empresa %s\n", entra(), cabecalhos["Org"])

	// Listagens vão por POST: o filtro não cabe no endereço.
	var catalogo struct {
		ProdutoList []produto `json:"produtoList"`
	}
	chama("POST", "/api/produtos/list", map[string]any{"pageSize": 5}, &catalogo)
	fmt.Println("\nProdutos:")
	for _, p := range catalogo.ProdutoList {
		fmt.Printf("  %-45s R$ %10.2f\n", p.Nome, p.ValorUnitario)
	}

	var clientes struct {
		PersonList []pessoa `json:"personList"`
	}
	chama("POST", "/api/pessoas/list", map[string]any{"tipo": "customer", "pageSize": 5}, &clientes)
	fmt.Println("\nClientes:")
	for _, c := range clientes.PersonList {
		fmt.Printf("  %-45s %s\n", c.Name, c.CpfCnpj)
	}

	var ultimos struct {
		PedidoList []pedido `json:"pedidoList"`
	}
	chama("POST", "/api/pedidos/list", map[string]any{
		"types":  []string{"ped"},
		"filter": map[string]any{"limit": 5, "orderBy": []map[string]string{{"fieldName": "createdAt", "direction": "DESC"}}},
	}, &ultimos)
	fmt.Println("\nÚltimos pedidos:")
	for _, p := range ultimos.PedidoList {
		fmt.Printf("  nº %-6d %-38s R$ %10.2f  %s\n", p.Numero, p.Pessoa.Nome, p.ValorTotal, p.Situacao)
	}

	switch {
	case os.Getenv("GRAVAR") != "1":
		fmt.Println("\nPara cadastrar um cliente e lançar um pedido de exemplo, rode de novo com GRAVAR=1.")
	case len(catalogo.ProdutoList) == 0:
		fmt.Println("\nCadastre ao menos um produto para lançar o pedido de exemplo.")
	default:
		lancado := lancaPedido(clienteDeExemplo(), catalogo.ProdutoList[0])
		fmt.Printf("\nPedido nº %d lançado para %s: R$ %.2f (%s)\n", lancado.Numero, lancado.Pessoa.Nome, lancado.ValorTotal, lancado.Situacao)
	}
}

func para(formato string, args ...any) {
	fmt.Fprintf(os.Stderr, formato+"\n", args...)
	os.Exit(1)
}

func confere(err error) {
	if err != nil {
		para("%v", err)
	}
}
