package edgecmd

import (
	"crypto/sha256"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"

	"github.com/spf13/cobra"

	"iop/packages/go/config"
	"iop/packages/go/version"
)

func bootstrapCmd() *cobra.Command {
	c := &cobra.Command{
		Use:   "bootstrap",
		Short: "Prepare node bootstrap artifacts",
		Long: `Prepare node bootstrap artifacts served by iop-edge serve.

The default output directory is the configured bootstrap.artifact_dir resolved
beside the iop-edge binary. The directory is static and can be copied as-is.`,
	}
	c.AddCommand(bootstrapPackCmd())
	return c
}

func bootstrapPackCmd() *cobra.Command {
	var nodeBinary string
	var outputDir string
	var target string
	var artifactBaseURL string
	var edgeAddr string
	c := &cobra.Command{
		Use:   "pack",
		Short: "Pack iop-node into the configured bootstrap artifact directory",
		RunE: func(cmd *cobra.Command, _ []string) error {
			cfg, err := loadEdgeConfigForBootstrapPack(cmd)
			if err != nil {
				return err
			}
			adv := resolveAdvertiseHost(cfg.Server.AdvertiseHost)
			if nodeBinary == "" {
				nodeBinary = filepath.Join(binaryDir(), "iop-node")
			}
			if outputDir == "" {
				outputDir = ResolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir)
			}
			if target == "" {
				target = runtime.GOOS + "-" + runtime.GOARCH
			}
			if artifactBaseURL == "" {
				if cfg.Bootstrap.ArtifactBaseURL != "" {
					artifactBaseURL = strings.TrimSuffix(cfg.Bootstrap.ArtifactBaseURL, "/")
				} else {
					artifactBaseURL = defaultArtifactBaseURL(adv.Host, cfg.Bootstrap.Listen)
				}
			}
			if edgeAddr == "" {
				edgeAddr = addressFor(adv.Host, cfg.Server.Listen)
			}
			if err := packNodeBootstrap(nodeBootstrapPackOptions{
				NodeBinary:      nodeBinary,
				OutputDir:       outputDir,
				Target:          target,
				Version:         version.Version,
				ArtifactBaseURL: artifactBaseURL,
				EdgeAddr:        edgeAddr,
			}); err != nil {
				return err
			}
			fmt.Fprintf(cmd.OutOrStdout(), "wrote bootstrap artifacts to %s\n", outputDir)
			fmt.Fprintf(cmd.OutOrStdout(), "bootstrap: %s\n", universalBootstrapScriptURL(artifactBaseURL))
			return nil
		},
	}
	c.Flags().StringVar(&nodeBinary, "node-binary", "", "iop-node binary to pack (defaults to iop-node beside iop-edge)")
	c.Flags().StringVarP(&outputDir, "output", "o", "", "artifact output directory (defaults to bootstrap.artifact_dir)")
	c.Flags().StringVar(&target, "target", "", "target platform label (defaults to current GOOS-GOARCH)")
	c.Flags().StringVar(&artifactBaseURL, "artifact-base-url", "", "artifact base URL baked into the bootstrap script")
	c.Flags().StringVar(&edgeAddr, "edge-addr", "", "edge transport address baked into node.yaml")
	return c
}

func loadEdgeConfigForBootstrapPack(cmd *cobra.Command) (*config.EdgeConfig, error) {
	cfg, _, err := LoadEdgeConfig(cmd)
	if err == nil {
		return cfg, nil
	}
	if !strings.Contains(err.Error(), "no edge.yaml found") {
		return nil, err
	}
	return &config.EdgeConfig{
		Server: config.EdgeServerConf{
			Listen: "0.0.0.0:9090",
		},
		Bootstrap: config.EdgeBootstrapConf{
			Listen:      "0.0.0.0:18080",
			ArtifactDir: "artifacts",
		},
	}, nil
}

type nodeBootstrapPackOptions struct {
	NodeBinary      string
	OutputDir       string
	Target          string
	Version         string
	ArtifactBaseURL string
	EdgeAddr        string
}

func packNodeBootstrap(opts nodeBootstrapPackOptions) error {
	if opts.NodeBinary == "" {
		return errors.New("node binary path is required")
	}
	if opts.OutputDir == "" {
		return errors.New("output directory is required")
	}
	if opts.Target == "" {
		return errors.New("target is required")
	}
	if strings.ContainsAny(opts.Target, `/\`) {
		return fmt.Errorf("target %q must not contain path separators", opts.Target)
	}
	targetDir := filepath.Join(opts.OutputDir, opts.Target)
	if err := os.MkdirAll(targetDir, 0o755); err != nil {
		return fmt.Errorf("mkdir target dir: %w", err)
	}
	dstNode := filepath.Join(targetDir, "iop-node")
	same, err := sameFile(opts.NodeBinary, dstNode)
	if err != nil {
		return fmt.Errorf("stat iop-node: %w", err)
	}
	if same {
		if err := os.Chmod(dstNode, 0o755); err != nil {
			return fmt.Errorf("chmod iop-node: %w", err)
		}
	} else {
		if err := copyFile(opts.NodeBinary, dstNode, 0o755); err != nil {
			return fmt.Errorf("copy iop-node: %w", err)
		}
	}
	sum, err := fileSHA256(dstNode)
	if err != nil {
		return err
	}
	sumLine := fmt.Sprintf("%x  iop-node\n", sum)
	if err := os.WriteFile(filepath.Join(targetDir, "iop-node.sha256"), []byte(sumLine), 0o644); err != nil {
		return fmt.Errorf("write iop-node.sha256: %w", err)
	}
	if err := os.WriteFile(filepath.Join(targetDir, "SHA256SUMS"), []byte(sumLine), 0o644); err != nil {
		return fmt.Errorf("write SHA256SUMS: %w", err)
	}
	bootstrapDir := filepath.Join(opts.OutputDir, "bootstrap")
	if err := os.MkdirAll(bootstrapDir, 0o755); err != nil {
		return fmt.Errorf("mkdir bootstrap dir: %w", err)
	}
	script := nodeBootstrapScript(opts)
	scriptPath := filepath.Join(bootstrapDir, "node-"+opts.Target+".sh")
	if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
		return fmt.Errorf("write bootstrap script: %w", err)
	}
	if strings.HasPrefix(opts.Target, "windows-") {
		psScriptPath := filepath.Join(bootstrapDir, "node-"+opts.Target+".ps1")
		if err := os.WriteFile(psScriptPath, []byte(nodeBootstrapPowerShellScript(opts)), 0o644); err != nil {
			return fmt.Errorf("write powershell bootstrap script: %w", err)
		}
	}
	if err := writeUniversalBootstrapScript(bootstrapDir, opts); err != nil {
		return err
	}
	return nil
}

func writeUniversalBootstrapScript(bootstrapDir string, opts nodeBootstrapPackOptions) error {
	universalOpts := opts
	universalOpts.Target = ""
	scriptPath := filepath.Join(bootstrapDir, "node.sh")
	if err := os.MkdirAll(bootstrapDir, 0o755); err != nil {
		return fmt.Errorf("mkdir bootstrap dir: %w", err)
	}
	if err := os.WriteFile(scriptPath, []byte(nodeBootstrapScript(universalOpts)), 0o755); err != nil {
		return fmt.Errorf("write universal bootstrap script: %w", err)
	}
	return nil
}

func sameFile(a, b string) (bool, error) {
	ai, err := os.Stat(a)
	if err != nil {
		return false, err
	}
	bi, err := os.Stat(b)
	if errors.Is(err, os.ErrNotExist) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return os.SameFile(ai, bi), nil
}

func copyFile(src, dst string, mode os.FileMode) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
	if err != nil {
		return err
	}
	if _, err := io.Copy(out, in); err != nil {
		_ = out.Close()
		return err
	}
	if err := out.Close(); err != nil {
		return err
	}
	return os.Chmod(dst, mode)
}

func fileSHA256(path string) ([]byte, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("open checksum file: %w", err)
	}
	defer f.Close()
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return nil, fmt.Errorf("hash file: %w", err)
	}
	return h.Sum(nil), nil
}

func shellQuote(s string) string {
	return strconv.Quote(s)
}

func powershellQuote(s string) string {
	return "'" + strings.ReplaceAll(s, "'", "''") + "'"
}

func bootstrapCommand(artifactURL, target, token string) string {
	if strings.HasPrefix(target, "windows-") {
		return fmt.Sprintf("Invoke-RestMethod %s | Invoke-Expression; Start-IopNode %s", bootstrapPowerShellScriptURL(artifactURL, target), powershellQuote(token))
	}
	return fmt.Sprintf("curl -fsSL %s | bash -s %s", universalBootstrapScriptURL(artifactURL), token)
}

func nodeBootstrapScript(opts nodeBootstrapPackOptions) string {
	return `#!/usr/bin/env bash
set -euo pipefail

VERSION=` + shellQuote(opts.Version) + `
DEFAULT_TARGET=` + shellQuote(opts.Target) + `
DEFAULT_ARTIFACT_BASE_URL=` + shellQuote(strings.TrimSuffix(opts.ArtifactBaseURL, "/")) + `
DEFAULT_EDGE_ADDR=` + shellQuote(opts.EdgeAddr) + `
POSITIONAL_NODE_TOKEN="${1:-}"
TARGET="$DEFAULT_TARGET"
ARTIFACT_BASE_URL="$DEFAULT_ARTIFACT_BASE_URL"
EDGE_ADDR="$DEFAULT_EDGE_ADDR"
NODE_TOKEN="$POSITIONAL_NODE_TOKEN"
INSTALL_DIR="${IOP_HOME:-$HOME/iop-field}"
CONFIG_FILE="${IOP_NODE_CONFIG:-$INSTALL_DIR/node.yaml}"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/iop-node-bootstrap.XXXXXX")"

detect_target() {
  local os arch
  case "$(uname -s)" in
    Linux*) os="linux" ;;
    Darwin*) os="darwin" ;;
    MINGW*|MSYS*|CYGWIN*) os="windows" ;;
    *)
      echo "[iop-bootstrap] unsupported OS: $(uname -s)" >&2
      return 2
      ;;
  esac

  case "$(uname -m)" in
    x86_64|amd64) arch="amd64" ;;
    arm64|aarch64) arch="arm64" ;;
    *)
      echo "[iop-bootstrap] unsupported architecture: $(uname -m)" >&2
      return 2
      ;;
  esac

  printf '%s-%s\n' "$os" "$arch"
}

cleanup() {
  rm -rf "$TMP_DIR"
}
trap cleanup EXIT

ARTIFACT_BASE_URL="${ARTIFACT_BASE_URL%/}"

if [[ -z "$NODE_TOKEN" ]]; then
  echo "[iop-bootstrap] missing node token" >&2
  echo "[iop-bootstrap] rerun the complete bootstrap command printed by iop-edge node register" >&2
  exit 2
fi

if [[ -z "$TARGET" ]]; then
  TARGET="$(detect_target)"
fi

BIN_FILE="$INSTALL_DIR/iop-node"
if [[ "$TARGET" == windows-* ]]; then
  BIN_FILE="$INSTALL_DIR/iop-node.exe"
fi

echo "[iop-bootstrap] version=$VERSION target=$TARGET"
echo "[iop-bootstrap] install_dir=$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"

curl -fsSL "$ARTIFACT_BASE_URL/$TARGET/iop-node" -o "$TMP_DIR/iop-node"
curl -fsSL "$ARTIFACT_BASE_URL/$TARGET/SHA256SUMS" -o "$TMP_DIR/SHA256SUMS"

if command -v sha256sum >/dev/null 2>&1; then
  (cd "$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | sha256sum -c -)
elif command -v shasum >/dev/null 2>&1; then
  (cd "$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | shasum -a 256 -c -)
else
  echo "[iop-bootstrap] warning: no sha256 checker found; skipping checksum verification" >&2
fi

cp "$TMP_DIR/iop-node" "$BIN_FILE"
chmod +x "$BIN_FILE"
if command -v xattr >/dev/null 2>&1; then
  xattr -d com.apple.quarantine "$BIN_FILE" 2>/dev/null || true
fi

cat > "$CONFIG_FILE" <<YAML
transport:
  edge_addr: "$EDGE_ADDR"
  token: "$NODE_TOKEN"

logging:
  level: "${IOP_LOG_LEVEL:-info}"
  pretty: true

metrics:
  port: ${IOP_NODE_METRICS_PORT:-9091}
YAML

echo "[iop-bootstrap] config=$CONFIG_FILE"
echo "[iop-bootstrap] starting node against $EDGE_ADDR"
cd "$INSTALL_DIR"
exec "$BIN_FILE" serve --config "$CONFIG_FILE"
`
}

func nodeBootstrapPowerShellScript(opts nodeBootstrapPackOptions) string {
	return `$ErrorActionPreference = 'Stop'

function Start-IopNode {
  param(
    [Parameter(Mandatory = $true, Position = 0)]
    [string] $Token
  )

  $Version = ` + powershellQuote(opts.Version) + `
  $Target = ` + powershellQuote(opts.Target) + `
  $ArtifactBaseURL = ` + powershellQuote(strings.TrimSuffix(opts.ArtifactBaseURL, "/")) + `
  $EdgeAddr = ` + powershellQuote(opts.EdgeAddr) + `
  $InstallDir = Join-Path $HOME 'iop-field'
  $ConfigFile = Join-Path $InstallDir 'node.yaml'
  $LogFile = Join-Path $InstallDir 'iop-node.log'
  $LogFileYAML = $LogFile.Replace('\', '/')
  $StdoutLog = Join-Path $InstallDir 'iop-node.stdout.log'
  $StderrLog = Join-Path $InstallDir 'iop-node.stderr.log'
  $TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ('iop-node-bootstrap-' + [guid]::NewGuid().ToString('N'))

  New-Item -ItemType Directory -Force -Path $InstallDir, $TmpDir | Out-Null
  try {
    $TmpNode = Join-Path $TmpDir 'iop-node'
    Invoke-WebRequest -UseBasicParsing -Uri "$ArtifactBaseURL/$Target/iop-node" -OutFile $TmpNode
    $Sums = Invoke-RestMethod -Uri "$ArtifactBaseURL/$Target/SHA256SUMS"
    $Expected = (($Sums -split '\r?\n' | Where-Object { $_ -match '\s+iop-node$' } | Select-Object -First 1) -split '\s+')[0]
    if ([string]::IsNullOrWhiteSpace($Expected)) {
      throw 'checksum entry for iop-node not found'
    }
    $Actual = (Get-FileHash -Algorithm SHA256 -Path $TmpNode).Hash.ToLowerInvariant()
    if ($Actual -ne $Expected.ToLowerInvariant()) {
      throw "checksum mismatch for iop-node: expected $Expected got $Actual"
    }

    $BinFile = Join-Path $InstallDir 'iop-node.exe'
    Copy-Item -Force $TmpNode $BinFile

    @"
transport:
  edge_addr: "$EdgeAddr"
  token: "$Token"

logging:
  level: "info"
  pretty: true
  path: "$LogFileYAML"

metrics:
  port: 19104
"@ | Set-Content -Encoding UTF8 -Path $ConfigFile

    $Process = Start-Process -FilePath $BinFile -ArgumentList @('serve', '--config', $ConfigFile) -WorkingDirectory $InstallDir -RedirectStandardOutput $StdoutLog -RedirectStandardError $StderrLog -WindowStyle Hidden -PassThru
    "[iop-bootstrap] version=$Version target=$Target pid=$($Process.Id)"
    "[iop-bootstrap] config=$ConfigFile log=$LogFile"
  } finally {
    Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
  }
}
`
}
