package production

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	ethcrypto "github.com/ethereum/go-ethereum/crypto"
)

// PreBuiltManifest describes the contents of a pre-built binary archive.
type PreBuiltManifest struct {
	Version   string            `json:"version"`
	Commit    string            `json:"commit"`
	Date      string            `json:"date"`
	Arch      string            `json:"arch"`
	Checksums map[string]string `json:"checksums"` // filename -> sha256
}

// HasPreBuiltArchive checks if a pre-built binary archive has been extracted
// at /opt/orama/ by looking for the manifest.json file.
func HasPreBuiltArchive() bool {
	_, err := os.Stat(OramaManifest)
	return err == nil
}

// LoadPreBuiltManifest loads and parses the pre-built manifest.
func LoadPreBuiltManifest() (*PreBuiltManifest, error) {
	data, err := os.ReadFile(OramaManifest)
	if err != nil {
		return nil, fmt.Errorf("failed to read manifest: %w", err)
	}

	var manifest PreBuiltManifest
	if err := json.Unmarshal(data, &manifest); err != nil {
		return nil, fmt.Errorf("failed to parse manifest: %w", err)
	}

	return &manifest, nil
}

// OramaSignerAddress is the Ethereum address authorized to sign build archives.
// Archives signed by any other address are rejected during install.
// This is the DeBros deploy wallet — update if the signing key rotates.
const OramaSignerAddress = "0xb5d8a496c8b2412990d7D467E17727fdF5954afC"

// VerifyArchiveSignature verifies that the pre-built archive was signed by the
// authorized Orama signer. Returns nil if the signature is valid, or if no
// signature file exists (unsigned archives are allowed but logged as a warning).
func VerifyArchiveSignature(manifest *PreBuiltManifest) error {
	sigData, err := os.ReadFile(OramaManifestSig)
	if os.IsNotExist(err) {
		return nil // unsigned archive — caller decides whether to proceed
	}
	if err != nil {
		return fmt.Errorf("failed to read manifest.sig: %w", err)
	}

	// Reproduce the same hash used during signing: SHA256 of compact JSON
	manifestJSON, err := json.Marshal(manifest)
	if err != nil {
		return fmt.Errorf("failed to marshal manifest: %w", err)
	}
	manifestHash := sha256.Sum256(manifestJSON)
	hashHex := hex.EncodeToString(manifestHash[:])

	// EVM personal_sign: keccak256("\x19Ethereum Signed Message:\n" + len + message)
	msg := []byte(hashHex)
	prefix := []byte("\x19Ethereum Signed Message:\n" + fmt.Sprintf("%d", len(msg)))
	ethHash := ethcrypto.Keccak256(prefix, msg)

	// Decode signature
	sigHex := strings.TrimSpace(string(sigData))
	if strings.HasPrefix(sigHex, "0x") || strings.HasPrefix(sigHex, "0X") {
		sigHex = sigHex[2:]
	}
	sig, err := hex.DecodeString(sigHex)
	if err != nil || len(sig) != 65 {
		return fmt.Errorf("invalid signature format in manifest.sig")
	}

	// Normalize recovery ID
	if sig[64] >= 27 {
		sig[64] -= 27
	}

	// Recover public key from signature
	pub, err := ethcrypto.SigToPub(ethHash, sig)
	if err != nil {
		return fmt.Errorf("signature recovery failed: %w", err)
	}

	recovered := ethcrypto.PubkeyToAddress(*pub).Hex()
	expected := strings.ToLower(OramaSignerAddress)
	got := strings.ToLower(recovered)

	if got != expected {
		return fmt.Errorf("archive signed by %s, expected %s — refusing to install", recovered, OramaSignerAddress)
	}

	return nil
}

// IsArchiveSigned returns true if a manifest.sig file exists alongside the manifest.
func IsArchiveSigned() bool {
	_, err := os.Stat(OramaManifestSig)
	return err == nil
}

// installFromPreBuilt installs all binaries from a pre-built archive.
// The archive must already be extracted at /opt/orama/ with:
//   - /opt/orama/bin/ — all pre-compiled binaries
//   - /opt/orama/systemd/ — namespace service templates
//   - /opt/orama/packages/ — optional .deb packages
//   - /opt/orama/manifest.json — archive metadata
func (ps *ProductionSetup) installFromPreBuilt(manifest *PreBuiltManifest) error {
	ps.logf("  Using pre-built binary archive v%s (%s) linux/%s", manifest.Version, manifest.Commit, manifest.Arch)

	// Verify archive signature if present
	if IsArchiveSigned() {
		if err := VerifyArchiveSignature(manifest); err != nil {
			return fmt.Errorf("archive signature verification failed: %w", err)
		}
		ps.logf("  ✓ Archive signature verified")
	} else {
		ps.logf("  ⚠️  Archive is unsigned — consider using 'orama build --sign'")
	}

	// Install minimal system dependencies (no build tools needed)
	if err := ps.installMinimalSystemDeps(); err != nil {
		ps.logf("  ⚠️  System dependencies warning: %v", err)
	}

	// Copy binaries to runtime locations
	if err := ps.deployPreBuiltBinaries(manifest); err != nil {
		return fmt.Errorf("failed to deploy pre-built binaries: %w", err)
	}

	// Set capabilities on binaries that need to bind privileged ports
	if err := ps.setCapabilities(); err != nil {
		return fmt.Errorf("failed to set capabilities: %w", err)
	}

	// Install ntfy on every node (feature #72). ntfy is not bundled in
	// the pre-built archive — its installer downloads from upstream and
	// verifies the SHA-256 checksum. ntfy listens on
	// 127.0.0.1:NtfyListenPort only (no public exposure), so it's safe
	// to run cluster-wide; nodes that don't serve a public push.* DNS
	// entry just have an idle ntfy with no inbound traffic. Uniform
	// install means no per-node toggling and no surprises when DNS
	// topology changes.
	//
	// Note: this must run BEFORE Phase 4's ConfigureNtfy, otherwise the
	// chown of /etc/ntfy/server.yml fails because the `ntfy` user
	// doesn't exist yet.
	if err := ps.binaryInstaller.InstallNtfy(); err != nil {
		ps.logf("  ⚠️  ntfy install warning: %v", err)
	}

	// Disable systemd-resolved stub listener for nameserver nodes
	// (needed even in pre-built mode so CoreDNS can bind port 53)
	if ps.isNameserver {
		if err := ps.disableResolvedStub(); err != nil {
			ps.logf("  ⚠️  Failed to disable systemd-resolved stub: %v", err)
		}
	}

	// Install Anyone relay from .deb package if available
	if ps.IsAnyoneRelay() || ps.IsAnyoneClient() {
		if err := ps.installAnyonFromPreBuilt(); err != nil {
			ps.logf("  ⚠️  Anyone install warning: %v", err)
		}
	}

	ps.logf("  ✓ All pre-built binaries installed")
	return nil
}

// installMinimalSystemDeps installs only runtime dependencies (no build tools).
func (ps *ProductionSetup) installMinimalSystemDeps() error {
	ps.logf("  Installing minimal system dependencies...")

	cmd := exec.Command("apt-get", "update")
	if err := cmd.Run(); err != nil {
		ps.logf("    Warning: apt update failed")
	}

	// Only install runtime deps — no build-essential, make, nodejs, npm needed
	cmd = exec.Command("apt-get", "install", "-y", "curl", "wget", "unzip")
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to install minimal dependencies: %w", err)
	}

	ps.logf("  ✓ Minimal system dependencies installed (no build tools needed)")
	return nil
}

// deployPreBuiltBinaries copies pre-built binaries to their runtime locations.
func (ps *ProductionSetup) deployPreBuiltBinaries(manifest *PreBuiltManifest) error {
	ps.logf("  Deploying pre-built binaries...")

	// Binary → destination mapping
	// Most go to /usr/local/bin/, caddy goes to /usr/bin/
	type binaryDest struct {
		name string
		dest string
	}

	binaries := []binaryDest{
		{name: "orama", dest: "/usr/local/bin/orama"},
		{name: "orama-node", dest: "/usr/local/bin/orama-node"},
		{name: "gateway", dest: "/usr/local/bin/gateway"},
		{name: "identity", dest: "/usr/local/bin/identity"},
		{name: "sfu", dest: "/usr/local/bin/sfu"},
		{name: "turn", dest: "/usr/local/bin/turn"},
		{name: "olric-server", dest: "/usr/local/bin/olric-server"},
		{name: "ipfs", dest: "/usr/local/bin/ipfs"},
		{name: "ipfs-cluster-service", dest: "/usr/local/bin/ipfs-cluster-service"},
		{name: "rqlited", dest: "/usr/local/bin/rqlited"},
		{name: "coredns", dest: "/usr/local/bin/coredns"},
		{name: "caddy", dest: "/usr/bin/caddy"},
	}
	// Note: vault-guardian stays at /opt/orama/bin/ (from archive extraction)
	// and is referenced by absolute path in the systemd service — no copy needed.

	for _, bin := range binaries {
		srcPath := filepath.Join(OramaArchiveBin, bin.name)

		// Skip optional binaries (e.g., coredns on non-nameserver nodes)
		if _, ok := manifest.Checksums[bin.name]; !ok {
			continue
		}

		if _, err := os.Stat(srcPath); os.IsNotExist(err) {
			ps.logf("    ⚠️  Binary %s not found in archive, skipping", bin.name)
			continue
		}

		if err := copyBinary(srcPath, bin.dest); err != nil {
			return fmt.Errorf("failed to copy %s: %w", bin.name, err)
		}
		ps.logf("    ✓ %s → %s", bin.name, bin.dest)
	}

	return nil
}

// setCapabilities sets cap_net_bind_service on binaries that need to bind privileged ports.
// Both the /opt/orama/bin/ originals (used by systemd) and /usr/local/bin/ copies need caps.
func (ps *ProductionSetup) setCapabilities() error {
	caps := []string{
		filepath.Join(OramaArchiveBin, "orama-node"), // systemd uses this path
		"/usr/local/bin/orama-node",                  // PATH copy
		"/usr/bin/caddy",                             // caddy's standard location
	}
	for _, binary := range caps {
		if _, err := os.Stat(binary); os.IsNotExist(err) {
			continue
		}
		cmd := exec.Command("setcap", "cap_net_bind_service=+ep", binary)
		if err := cmd.Run(); err != nil {
			return fmt.Errorf("setcap failed on %s: %w (node won't be able to bind port 443)", binary, err)
		}
		ps.logf("    ✓ setcap on %s", binary)
	}
	return nil
}

// disableResolvedStub disables systemd-resolved's stub listener so CoreDNS can bind port 53.
func (ps *ProductionSetup) disableResolvedStub() error {
	// Delegate to the coredns installer's method
	return ps.binaryInstaller.coredns.DisableResolvedStubListener()
}

// installAnyonFromPreBuilt installs the Anyone relay .deb from the packages dir,
// falling back to apt install if the .deb is not bundled.
func (ps *ProductionSetup) installAnyonFromPreBuilt() error {
	debPath := filepath.Join(OramaPackagesDir, "anon.deb")
	if _, err := os.Stat(debPath); err == nil {
		ps.logf("  Installing Anyone from bundled .deb...")
		cmd := exec.Command("dpkg", "-i", debPath)
		if err := cmd.Run(); err != nil {
			ps.logf("  ⚠️  dpkg -i failed, falling back to apt...")
			cmd = exec.Command("apt-get", "install", "-y", "anon")
			if err := cmd.Run(); err != nil {
				return fmt.Errorf("failed to install anon: %w", err)
			}
		}
		ps.logf("  ✓ Anyone installed from .deb")
		return nil
	}

	// No .deb bundled — fall back to apt (the existing path in source mode)
	ps.logf("  Installing Anyone via apt (not bundled in archive)...")
	cmd := exec.Command("apt-get", "install", "-y", "anon")
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to install anon via apt: %w", err)
	}
	ps.logf("  ✓ Anyone installed via apt")
	return nil
}

// copyBinary copies a file from src to dest, preserving executable permissions.
// It removes the destination first to avoid ETXTBSY ("text file busy") errors
// when overwriting a binary that is currently running.
func copyBinary(src, dest string) error {
	// Ensure parent directory exists
	if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
		return err
	}

	// Remove the old binary first. On Linux, if the binary is running,
	// rm unlinks the filename while the kernel keeps the inode alive for
	// the running process. Writing a new file at the same path creates a
	// fresh inode — no ETXTBSY conflict.
	_ = os.Remove(dest)

	srcFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer srcFile.Close()

	destFile, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
	if err != nil {
		return err
	}
	defer destFile.Close()

	if _, err := io.Copy(destFile, srcFile); err != nil {
		return err
	}

	return nil
}
