//go:build mage

// Magefile for olcrtc.
//
// Quick reference:
//
//	mage check          # build + vet + lint + unit tests (pre-commit)
//	mage all            # full pre-merge pipeline (check + e2e smoke matrix)
//	mage nightly        # everything including stress matrix (~6h)
//
//	mage build          # native binary
//	mage cross          # all platforms
//	mage mobile         # Android AAR
//
//	mage test           # short unit tests
//	mage testfull       # all unit tests, no real providers
//	mage e2e            # real-provider smoke matrix
//	mage stress         # real-provider stress matrix (long)
//	mage soak           # real-provider throughput soak (long)
//	mage localsoak      # local in-memory soak (very long, no network)
//
// Tunables (env):
//
//	E2E_CARRIERS, E2E_TRANSPORTS, E2E_TIMEOUT
//	E2E_STRESS, E2E_STRESS_DURATION
//	STRESS_BULK_DURATION, STRESS_ECHO_DURATION, STRESS_CASE_TIMEOUT, STRESS_TIMEOUT
//	SOAK_CARRIERS, SOAK_TRANSPORTS, SOAK_DURATION, SOAK_CHAOS

package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/magefile/mage/mg"
	"github.com/magefile/mage/sh"
)

// Default target when invoked as `mage` with no args.
//
//nolint:gochecknoglobals // mage requires a package-level Default symbol.
var Default = Help

const (
	buildDir = "build"
	ldflags  = "-s -w"
)

var (
	goexe  = mg.GoCmd()
	goos   = envOr("GOOS", runtime.GOOS)
	goarch = envOr("GOARCH", runtime.GOARCH)
)

// ─────────────────────────────────────────────────────────────────────────────
// Pipelines
// ─────────────────────────────────────────────────────────────────────────────

// Help lists every target in mage's default style. This is what runs when
// you invoke `mage` with no arguments. For grouped/annotated docs see the
// magefile header.
func Help() error {
	return sh.RunV("mage", "-l")
}

// Check runs the fast pre-commit pipeline: build + vet + lint + unit tests.
// Use this before every commit.
func Check() {
	mg.SerialDeps(Build, Vet, Lint, TestFull)
}

// All runs the full pre-merge pipeline: Check + the real-provider smoke
// matrix. Stress and soak are NOT included - run them explicitly when
// needed (see Nightly for everything).
func All() {
	mg.SerialDeps(Check, E2e)
}

// Nightly runs everything: All + stress matrix. Expect multi-hour runtime;
// intended for the nightly CI job or manual deep validation.
func Nightly() {
	mg.SerialDeps(All, Stress)
}

// Everything runs literally every test stage: Nightly + real soak + local
// soak. Expect 12+ hours; this is for "I want maximum confidence before
// shipping" runs, not regular development. Tune SOAK_DURATION etc. if you
// need a shorter window.
func Everything() {
	mg.SerialDeps(Nightly, Soak, LocalSoak)
}

// ─────────────────────────────────────────────────────────────────────────────
// Build
// ─────────────────────────────────────────────────────────────────────────────

// Build builds the olcrtc CLI binary for the host platform.
func Build() error {
	mg.Deps(Deps)
	return buildBinary("olcrtc", "./cmd/olcrtc", goos, goarch)
}

// Cross builds olcrtc for all supported platforms.
func Cross() error {
	mg.Deps(Deps)

	targets := []struct{ os, arch string }{
		{"linux", "amd64"},
		{"linux", "arm64"},
		{"windows", "amd64"},
		{"darwin", "amd64"},
		{"darwin", "arm64"},
		{"freebsd", "amd64"},
		{"freebsd", "arm64"},
		{"openbsd", "amd64"},
		{"openbsd", "arm64"},
	}

	for _, t := range targets {
		if err := buildBinary("olcrtc", "./cmd/olcrtc", t.os, t.arch); err != nil {
			return err
		}
	}

	fmt.Printf("✅ built %d platform(s)\n", len(targets))
	return nil
}

// Mobile builds the Android AAR via gomobile.
func Mobile() error {
	if err := ensureTool("gomobile"); err != nil {
		return fmt.Errorf("gomobile not found: run 'go install golang.org/x/mobile/cmd/gomobile@latest && gomobile init'")
	}
	if err := ensureBuildDir(); err != nil {
		return err
	}
	return sh.RunV("gomobile", "bind",
		"-target=android",
		"-androidapi", "21",
		"-ldflags", "-s -w -checklinkname=0",
		"-o", filepath.Join(buildDir, "olcrtc.aar"),
		"./mobile",
	)
}

// ─────────────────────────────────────────────────────────────────────────────
// Container
// ─────────────────────────────────────────────────────────────────────────────

// Docker builds the image using docker.
func Docker() error {
	tag := envOr("DOCKER_TAG", "olcrtc:latest")
	return sh.RunV("docker", "build", "-t", tag, ".")
}

// Podman builds the image using podman.
func Podman() error {
	tag := envOr("DOCKER_TAG", "olcrtc:latest")
	return sh.RunV("podman", "build", "-t", tag, ".")
}

// ─────────────────────────────────────────────────────────────────────────────
// Quality
// ─────────────────────────────────────────────────────────────────────────────

// Vet runs go vet on the whole module.
func Vet() error {
	return sh.RunV(goexe, "vet", "./...")
}

// Lint runs golangci-lint.
func Lint() error {
	if err := ensureTool("golangci-lint"); err != nil {
		return fmt.Errorf("golangci-lint not found, install it:\n  go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest")
	}
	return sh.RunV("golangci-lint", "run", "./...")
}

// Tidy runs go mod tidy and verifies modules.
func Tidy() error {
	if err := sh.RunV(goexe, "mod", "tidy"); err != nil {
		return err
	}
	return sh.RunV(goexe, "mod", "verify")
}

// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────

// Test runs unit tests in -short mode (skips long-running tests).
func Test() error {
	return sh.RunV(goexe, "test", "-race", "-count=1", "-short", "./...")
}

// TestFull runs all unit + fast e2e tests with race detector. No real providers.
func TestFull() error {
	return sh.RunV(goexe, "test", "-race", "-count=1", "-timeout", "10m", "./...")
}

// E2e runs the real-provider smoke matrix.
// Configure via env: E2E_CARRIERS, E2E_TRANSPORTS, E2E_TIMEOUT, E2E_STRESS.
//
// Note: -race is intentionally NOT enabled here. The race detector adds
// significant CPU overhead and breaks timing-sensitive handshake tests
// against real networks (telemost/videochannel handshake regularly times
// out under -race). Race coverage for production code paths is provided
// by `mage testFull` against in-memory carriers.
func E2e() error {
	args := []string{"test", "-count=1", "-v", "-timeout", "30m",
		"./internal/e2e/...",
		"-olcrtc.real-e2e=true",
	}
	if carriers := os.Getenv("E2E_CARRIERS"); carriers != "" {
		args = append(args, "-olcrtc.real-carriers="+carriers)
	}
	if transports := os.Getenv("E2E_TRANSPORTS"); transports != "" {
		args = append(args, "-olcrtc.real-transports="+transports)
	}
	if timeout := os.Getenv("E2E_TIMEOUT"); timeout != "" {
		args = append(args, "-olcrtc.real-timeout="+timeout)
	}
	if os.Getenv("E2E_STRESS") != "" {
		args = append(args, "-olcrtc.stress=true")
		if d := os.Getenv("E2E_STRESS_DURATION"); d != "" {
			args = append(args, "-olcrtc.stress-duration="+d)
		}
	}
	return sh.RunV(goexe, args...)
}

// Stress runs the real-provider stress matrix on every carrier × transport pair.
// Defaults match the long nightly profile (15m bulk + 15m echo, 35m hard cap per case).
// Override via env: STRESS_BULK_DURATION, STRESS_ECHO_DURATION, STRESS_CASE_TIMEOUT,
// STRESS_TIMEOUT, E2E_CARRIERS, E2E_TRANSPORTS.
func Stress() error {
	bulk := envOr("STRESS_BULK_DURATION", "15m")
	echo := envOr("STRESS_ECHO_DURATION", "15m")
	caseTO := envOr("STRESS_CASE_TIMEOUT", "35m")
	overall := envOr("STRESS_TIMEOUT", "6h")

	args := []string{"test", "-count=1", "-v",
		"-timeout", overall,
		"-run", "^TestRealProviderTransportStress$",
		"./internal/e2e/...",
		"-olcrtc.real-e2e=true",
		"-olcrtc.stress=true",
		"-olcrtc.stress-bulk-duration=" + bulk,
		"-olcrtc.stress-duration=" + echo,
		"-olcrtc.stress-case-timeout=" + caseTO,
	}
	if carriers := os.Getenv("E2E_CARRIERS"); carriers != "" {
		args = append(args, "-olcrtc.real-carriers="+carriers)
	}
	if transports := os.Getenv("E2E_TRANSPORTS"); transports != "" {
		args = append(args, "-olcrtc.real-transports="+transports)
	}
	return sh.RunV(goexe, args...)
}

// Soak runs the real-provider throughput soak test.
// Configure via env: SOAK_CARRIERS, SOAK_TRANSPORTS, SOAK_DURATION.
func Soak() error {
	carriers := envOr("SOAK_CARRIERS", "telemost,jitsi,wbstream")
	transports := envOr("SOAK_TRANSPORTS", "datachannel,vp8channel")
	duration := envOr("SOAK_DURATION", "10m")

	args := []string{"test", "-count=1", "-v",
		"-timeout", "12h",
		"-run", "^TestRealThroughputSoak$",
		"./internal/e2e/...",
		"-olcrtc.real-e2e=true",
		"-olcrtc.real-soak=true",
		"-olcrtc.real-soak-carrier=" + carriers,
		"-olcrtc.real-soak-transport=" + transports,
		"-olcrtc.real-soak-duration=" + duration,
	}
	return sh.RunV(goexe, args...)
}

// LocalSoak runs the local (in-memory) throughput soak.
// Configure via env: SOAK_TRANSPORTS, SOAK_DURATION, SOAK_CHAOS.
func LocalSoak() error {
	transports := envOr("SOAK_TRANSPORTS", "all")
	duration := envOr("SOAK_DURATION", "6m")
	chaos := os.Getenv("SOAK_CHAOS")

	args := []string{"test", "-count=1", "-v",
		"-timeout", "12h",
		"-run", "^TestLocalThroughputSoak$",
		"./internal/e2e/...",
		"-olcrtc.local-soak=true",
		"-olcrtc.local-soak-transport=" + transports,
		"-olcrtc.local-soak-duration=" + duration,
	}
	if chaos != "" {
		args = append(args, "-olcrtc.local-soak-chaos="+chaos)
	}
	return sh.RunV(goexe, args...)
}

// ─────────────────────────────────────────────────────────────────────────────
// Utility
// ─────────────────────────────────────────────────────────────────────────────

// Deps downloads Go module dependencies.
func Deps() error {
	return sh.RunV(goexe, "mod", "download")
}

// Clean removes build artifacts.
func Clean() error {
	return os.RemoveAll(buildDir)
}

// ─────────────────────────────────────────────────────────────────────────────
// Helpers (unexported, not visible as targets)
// ─────────────────────────────────────────────────────────────────────────────

func buildBinary(name, pkg, os_, arch string) error {
	if err := ensureBuildDir(); err != nil {
		return err
	}

	ext := ""
	if os_ == "windows" {
		ext = ".exe"
	}
	outName := fmt.Sprintf("%s-%s-%s%s", name, os_, arch, ext)
	out := filepath.Join(buildDir, outName)
	fmt.Printf("building %s (%s/%s) -> %s\n", name, os_, arch, out)

	env := map[string]string{
		"GOOS":        os_,
		"GOARCH":      arch,
		"CGO_ENABLED": "0",
	}

	flags := ldflags
	if os_ == "android" {
		flags += " -checklinkname=0"
	}

	args := []string{"build", "-trimpath", "-ldflags", flags, "-o", out, pkg}
	return sh.RunWithV(env, goexe, args...)
}

func ensureBuildDir() error {
	return os.MkdirAll(buildDir, 0o755)
}

func ensureTool(name string) error {
	_, err := exec.LookPath(name)
	return err
}

func envOr(key, fallback string) string {
	if v := strings.TrimSpace(os.Getenv(key)); v != "" {
		return v
	}
	return fallback
}
