package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"
)

const (
	baseURL          = "https://api.abacatepay.com/v1"
	defaultTimeout   = 15 * time.Second
	minWithdrawCents = 350
)

type apiEnvelope[T any] struct {
	Data  T               `json:"data"`
	Error json.RawMessage `json:"error"`
}

func getJSON[T any](path string, out *apiEnvelope[T]) error {
	apiKey := strings.TrimSpace(os.Getenv("ABACATEPAY_API_KEY"))
	if apiKey == "" {
		return errors.New("variável de ambiente ABACATEPAY_API_KEY não definida")
	}

	req, err := http.NewRequest(http.MethodGet, baseURL+path, nil)
	if err != nil {
		return err
	}

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Accept", "application/json")

	client := &http.Client{Timeout: defaultTimeout}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if resp.StatusCode >= 400 {
		return fmt.Errorf("requisição falhou (%d): %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
	}

	if err := json.Unmarshal(respBody, out); err != nil {
		return fmt.Errorf("falha ao parsear resposta: %w", err)
	}

	return nil
}

func runCheckPixCommand(args []string) error {
	if len(args) != 1 {
		return errors.New("uso: abacate-cli checkpix <pix_id>")
	}

	pixID := strings.TrimSpace(args[0])
	if pixID == "" {
		return errors.New("o id do QRCode PIX não pode ser vazio")
	}

	query := url.Values{}
	query.Set("id", pixID)

	var envelope apiEnvelope[pixQrCodeStatusResponse]
	if err := getJSON("/pixQrCode/check?"+query.Encode(), &envelope); err != nil {
		return err
	}

	if len(envelope.Error) > 0 && string(envelope.Error) != "null" {
		return fmt.Errorf("erro ao checar QRCode: %s", sanitizeAPIError(envelope.Error))
	}

	fmt.Printf("status=%s\n", envelope.Data.Status)
	return nil
}

type pixQrCodeRequest struct {
	Amount      int               `json:"amount"`
	ExpiresIn   int               `json:"expiresIn,omitempty"`
	Description string            `json:"description,omitempty"`
	Customer    *pixCustomerInput `json:"customer,omitempty"`
	Metadata    map[string]string `json:"metadata,omitempty"`
}

type pixCustomerInput struct {
	Name      string `json:"name"`
	Cellphone string `json:"cellphone"`
	Email     string `json:"email"`
	TaxID     string `json:"taxId"`
}

type pixQrCodeResponse struct {
	ID          string `json:"id"`
	Amount      int    `json:"amount"`
	Status      string `json:"status"`
	BRCode      string `json:"brCode"`
	BRCodeBase  string `json:"brCodeBase64"`
	PlatformFee int    `json:"platformFee"`
	CreatedAt   string `json:"createdAt"`
	ExpiresAt   string `json:"expiresAt"`
}

type pixQrCodeStatusResponse struct {
	Status    string `json:"status"`
	ExpiresAt string `json:"expiresAt"`
}

type withdrawRequest struct {
	ExternalID string          `json:"externalId"`
	Method     string          `json:"method"`
	Amount     int             `json:"amount"`
	Description string         `json:"description,omitempty"`
	Pix        withdrawPixData `json:"pix"`
}

type withdrawPixData struct {
	Type string `json:"type"`
	Key  string `json:"key"`
}

type withdrawResponse struct {
	ID          string `json:"id"`
	Status      string `json:"status"`
	Kind        string `json:"kind"`
	Amount      int    `json:"amount"`
	PlatformFee int    `json:"platformFee"`
	ReceiptURL  string `json:"receiptUrl"`
	ExternalID  string `json:"externalId"`
}

func main() {
	if err := loadEnvFile(".env"); err != nil {
		exitWithError(fmt.Errorf("falha ao carregar .env: %w", err))
	}

	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}

	cmd := os.Args[1]
	switch cmd {
	case "pix":
		if err := runPixCommand(os.Args[2:]); err != nil {
			exitWithError(err)
		}
	case "withdraw":
		if err := runWithdrawCommand(os.Args[2:]); err != nil {
			exitWithError(err)
		}
	case "checkpix":
		if err := runCheckPixCommand(os.Args[2:]); err != nil {
			exitWithError(err)
		}
	case "help", "-h", "--help":
		printUsage()
	default:
		exitWithError(fmt.Errorf("comando desconhecido: %s", cmd))
	}
}

func runPixCommand(args []string) error {
	if len(args) < 1 {
		return errors.New("uso: abacate-cli pix <valor> <descrição opcional> <tempo em segundos>")
	}
	if len(args) > 3 {
		return errors.New("muitos argumentos para o comando pix")
	}

	amountStr := args[0]
	description := ""
	expires := 1800

	if len(args) >= 2 {
		description = args[1]
	}
	if len(args) == 3 {
		secs, err := strconv.Atoi(args[2])
		if err != nil || secs <= 0 {
			return fmt.Errorf("tempo inválido: %q", args[2])
		}
		expires = secs
	}

	valueInCents, err := parseAmountToCents(amountStr)
	if err != nil {
		return err
	}

	req := pixQrCodeRequest{
		Amount:      valueInCents,
		ExpiresIn:   expires,
		Description: description,
	}

	var envelope apiEnvelope[pixQrCodeResponse]
	if err := postJSON("/pixQrCode/create", req, &envelope); err != nil {
		return err
	}

	if len(envelope.Error) > 0 && string(envelope.Error) != "null" {
		return fmt.Errorf("erro ao criar QRCode: %s", sanitizeAPIError(envelope.Error))
	}

	if envelope.Data.BRCode == "" || envelope.Data.ID == "" {
		return errors.New("resposta inválida: QRCode sem dados essenciais")
	}

	fmt.Println(envelope.Data.BRCode)
	fmt.Println("ID:", envelope.Data.ID)
	return nil
}

func runWithdrawCommand(args []string) error {
	if len(args) < 3 {
		return errors.New("uso: abacate-cli withdraw <valor> <tipo de chave> <chave> <descrição opcional>")
	}
	if len(args) > 4 {
		return errors.New("muitos argumentos para o comando withdraw")
	}

	amountStr := args[0]
	pixType := strings.ToUpper(args[1])
	pixKey := args[2]
	description := ""
	if len(args) == 4 {
		description = args[3]
	}

	valueInCents, err := parseAmountToCents(amountStr)
	if err != nil {
		return err
	}
	if valueInCents < minWithdrawCents {
		return fmt.Errorf("valor mínimo para saque é R$3,50")
	}

	req := withdrawRequest{
		ExternalID: fmt.Sprintf("withdraw-%d", time.Now().Unix()),
		Method:     "PIX",
		Amount:     valueInCents,
		Description: description,
		Pix: withdrawPixData{
			Type: pixType,
			Key:  pixKey,
		},
	}

	var envelope apiEnvelope[withdrawResponse]
	if err := postJSON("/withdraw/create", req, &envelope); err != nil {
		return err
	}

	if len(envelope.Error) > 0 && string(envelope.Error) != "null" {
		return fmt.Errorf("erro ao criar saque: %s", sanitizeAPIError(envelope.Error))
	}

	resp := envelope.Data
	if resp.ID == "" {
		return errors.New("resposta inválida: saque sem identificador")
	}

	fmt.Printf("SUCESSO %s status=%s amount=%0.2f receipt=%s\n",
		resp.ID,
		resp.Status,
		float64(resp.Amount)/100,
		resp.ReceiptURL,
	)
	return nil
}

func parseAmountToCents(amount string) (int, error) {
	s := strings.TrimSpace(amount)
	if s == "" {
		return 0, errors.New("valor vazio")
	}
	s = strings.ReplaceAll(s, ",", ".")
	value, err := strconv.ParseFloat(s, 64)
	if err != nil {
		return 0, fmt.Errorf("valor inválido: %w", err)
	}
	if value <= 0 {
		return 0, errors.New("o valor precisa ser maior que zero")
	}
	cents := int(value*100 + 0.5)
	return cents, nil
}

func postJSON[T any](path string, payload any, out *apiEnvelope[T]) error {
	apiKey := strings.TrimSpace(os.Getenv("ABACATEPAY_API_KEY"))
	if apiKey == "" {
		return errors.New("variável de ambiente ABACATEPAY_API_KEY não definida")
	}

	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	req, err := http.NewRequest(http.MethodPost, baseURL+path, bytes.NewBuffer(body))
	if err != nil {
		return err
	}

	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: defaultTimeout}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if resp.StatusCode >= 400 {
		return fmt.Errorf("requisição falhou (%d): %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
	}

	if err := json.Unmarshal(respBody, out); err != nil {
		return fmt.Errorf("falha ao parsear resposta: %w", err)
	}

	return nil
}

func sanitizeAPIError(raw json.RawMessage) string {
	if len(raw) == 0 || string(raw) == "null" {
		return ""
	}

	var asObj map[string]any
	if err := json.Unmarshal(raw, &asObj); err == nil {
		if msg, ok := asObj["message"].(string); ok && msg != "" {
			if code, ok := asObj["code"].(string); ok && code != "" {
				return fmt.Sprintf("%s (%s)", msg, code)
			}
			return msg
		}
		return string(raw)
	}

	var asStr string
	if err := json.Unmarshal(raw, &asStr); err == nil && asStr != "" {
		return asStr
	}

	return string(raw)
}

func printUsage() {
	fmt.Println(`Uso:
  abacate-cli pix <valor> <descrição opcional> <tempo em segundos>
  abacate-cli withdraw <valor> <tipo de chave> <chave> <descrição opcional>
  abacate-cli checkpix <pix_id>

Certifique-se de definir ABACATEPAY_API_KEY no ambiente.`)
}

func exitWithError(err error) {
	fmt.Fprintln(os.Stderr, "Erro:", err)
	os.Exit(1)
}

func loadEnvFile(path string) error {
	file, err := os.Open(path)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		key, value, found := strings.Cut(line, "=")
		if !found {
			continue
		}

		key = strings.TrimSpace(key)
		if key == "" {
			continue
		}

		value = strings.TrimSpace(value)
		value = strings.Trim(value, `"'`)

		if err := os.Setenv(key, value); err != nil {
			return err
		}
	}

	return scanner.Err()
}
