package config

import (
	"log"
	"os"
	"strconv"
	"strings"
)

// Agora credentials, read from the environment rather than from systemflag.
//
// The app certificate is a signing key: whoever holds it can mint a token for
// any channel and join any session. systemflag rows are readable by every
// existing flag reader and end up in any database dump, so the certificate
// lives alongside ADMIN_API_KEY in .env instead.
//
// The app id is not a secret — it ships inside the Flutter binaries — but it
// is kept with the certificate so the pair can never drift apart during a
// rotation.
type AgoraConfig struct {
	AppID          string
	AppCertificate string

	// Ceiling on a minted token's lifetime. The session-aware TTL is derived
	// from the consultation's own billing cap and then clamped to this.
	TokenTTLSeconds int

	// How far past the billing cap a token stays valid. The session can
	// outlive its cap by however late the sweep runs, and both apps still
	// need the link alive to exchange "session ended" and open the rating
	// screen.
	TokenGraceSeconds int
}

var Agora AgoraConfig

func LoadAgoraConfig() {

	Agora = AgoraConfig{

		AppID: strings.TrimSpace(os.Getenv("AGORA_APP_ID")),

		AppCertificate: strings.TrimSpace(os.Getenv("AGORA_APP_CERTIFICATE")),

		TokenTTLSeconds: envInt("AGORA_TOKEN_TTL_SECONDS", 3600),

		TokenGraceSeconds: envInt("AGORA_TOKEN_GRACE_SECONDS", 300),
	}
}

// Enabled reports whether tokens can be minted at all.
//
// A consultation is refused when this is false, rather than started without
// credentials. The alternative is worse: the request, the accept and the
// customer's confirmation would all succeed, the clock would start, and the
// customer would be billed for a session neither party could ever join.
func (c AgoraConfig) Enabled() bool {
	return c.AppID != "" && c.AppCertificate != ""
}

// Problem describes what is wrong with the configuration, or "" when it is
// usable.
//
// The shape check matters as much as the presence check. Agora's app id and
// certificate are both 32 hexadecimal characters, and a value that is merely
// present but malformed fails later, inside token minting, as an opaque
// "consultation service is not configured" — after an astrologer has already
// accepted. Catching it at boot is the difference between a log line and a
// support ticket.
func (c AgoraConfig) Problem() string {

	switch {

	case c.AppID == "" && c.AppCertificate == "":
		return "AGORA_APP_ID and AGORA_APP_CERTIFICATE are not set"

	case c.AppID == "":
		return "AGORA_APP_ID is not set"

	case c.AppCertificate == "":
		return "AGORA_APP_CERTIFICATE is not set"

	case !isAgoraCredentialShape(c.AppID):
		return "AGORA_APP_ID is not 32 hexadecimal characters"

	case !isAgoraCredentialShape(c.AppCertificate):
		return "AGORA_APP_CERTIFICATE is not 32 hexadecimal characters"
	}

	return ""
}

func isAgoraCredentialShape(value string) bool {

	if len(value) != 32 {
		return false
	}

	for _, char := range value {

		switch {
		case char >= '0' && char <= '9':
		case char >= 'a' && char <= 'f':
		case char >= 'A' && char <= 'F':
		default:
			return false
		}
	}

	return true
}

// LogAgoraConfig says at boot whether consultations can run at all.
//
// Same reasoning as LogFirebaseConfig: the failure mode otherwise is a live
// 400 from /consultation/start that reads as a code fault rather than as a
// .env the running service never read.
func LogAgoraConfig() {

	if problem := Agora.Problem(); problem != "" {

		log.Printf(
			"agora: %s - chat, audio and video consultations will be REFUSED "+
				"with \"consultation service is not configured\". Both values are "+
				"on the Agora console project page (App ID, and App Certificate "+
				"under Security). Set them in the .env this service actually "+
				"reads, then restart.",
			problem,
		)

		return
	}

	log.Printf(
		"agora: configured (app id %s…, token ttl %ds, grace %ds)",
		Agora.AppID[:8],
		Agora.TokenTTLSeconds,
		Agora.TokenGraceSeconds,
	)
}

func envInt(name string, fallback int) int {

	value := strings.TrimSpace(os.Getenv(name))

	if value == "" {
		return fallback
	}

	parsed, err := strconv.Atoi(value)

	if err != nil || parsed <= 0 {
		return fallback
	}

	return parsed
}
