package platform

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"time"

	"github.com/Eriyc/rules_wails/pkg/wails3kit/updates"
)

const helperFlag = "--wails3kit-update-helper"

type Detector func(app updates.AppDescriptor) (updates.InstallRoot, error)

type Installer struct {
	detector Detector
}

func New(detector Detector) *Installer {
	return &Installer{detector: detector}
}

func (installer *Installer) DetectInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) {
	if installer.detector == nil {
		return updates.InstallRoot{}, errors.New("missing install root detector")
	}
	return installer.detector(app)
}

func (installer *Installer) Stage(ctx context.Context, root updates.InstallRoot, file updates.DownloadedFile, release updates.Release) (updates.StagedArtifact, error) {
	stageDir, bundleManifest, err := extractBundle(ctx, file.Path, release.Artifact.Format)
	if err != nil {
		return updates.StagedArtifact{}, err
	}

	return updates.StagedArtifact{
		Path:    stageDir,
		Root:    root,
		Release: release,
		Bundle:  bundleManifest,
	}, nil
}

func (installer *Installer) SpawnApplyAndRestart(_ context.Context, req updates.ApplyRequest) error {
	helperDir, err := os.MkdirTemp("", "wails3kit-helper-*")
	if err != nil {
		return err
	}

	helperBinary := filepath.Join(helperDir, filepath.Base(req.App.ExecutablePath))
	if err := copyFile(req.App.ExecutablePath, helperBinary, 0o755); err != nil {
		return err
	}

	payload := helperRequest{
		InstallRoot:      req.Root.Path,
		StagedPath:       req.Staged.Path,
		Bundle:           req.Staged.Bundle,
		Args:             append([]string(nil), req.App.Args...),
		WorkingDirectory: req.App.WorkingDirectory,
	}

	requestPath := filepath.Join(helperDir, "request.json")
	encoded, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	if err := os.WriteFile(requestPath, encoded, 0o600); err != nil {
		return err
	}

	command := exec.Command(helperBinary, helperFlag, requestPath)
	command.Dir = helperDir
	command.Stdout = os.Stdout
	command.Stderr = os.Stderr
	return command.Start()
}

func MaybeRun(args []string) (bool, error) {
	if len(args) < 3 || args[1] != helperFlag {
		return false, nil
	}
	return true, runHelper(args[2])
}

func NewCurrent() (*Installer, error) {
	switch runtime.GOOS {
	case "darwin":
		return New(detectDarwinInstallRoot), nil
	case "windows":
		return New(detectDirectoryInstallRoot), nil
	case "linux":
		return New(detectDirectoryInstallRoot), nil
	default:
		return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS)
	}
}

type helperRequest struct {
	InstallRoot      string                `json:"installRoot"`
	StagedPath       string                `json:"stagedPath"`
	Bundle           updates.BundleManifest `json:"bundle"`
	Args             []string              `json:"args,omitempty"`
	WorkingDirectory string                `json:"workingDirectory,omitempty"`
}

func runHelper(requestPath string) error {
	bytes, err := os.ReadFile(requestPath)
	if err != nil {
		return err
	}

	var request helperRequest
	if err := json.Unmarshal(bytes, &request); err != nil {
		return err
	}

	deadline := time.Now().Add(15 * time.Second)
	var applyErr error
	for time.Now().Before(deadline) {
		applyErr = applyBundle(request)
		if applyErr == nil {
			break
		}
		time.Sleep(100 * time.Millisecond)
	}
	if applyErr != nil {
		return applyErr
	}

	relaunch := filepath.Join(request.InstallRoot, filepath.FromSlash(request.Bundle.EntryPoint))
	command := exec.Command(relaunch, request.Args...)
	if request.WorkingDirectory != "" {
		command.Dir = request.WorkingDirectory
	} else {
		command.Dir = request.InstallRoot
	}
	command.Stdout = os.Stdout
	command.Stderr = os.Stderr
	return command.Start()
}

func detectDirectoryInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) {
	if app.ExecutablePath == "" {
		return updates.InstallRoot{}, errors.New("missing executable path")
	}
	return updates.InstallRoot{Path: filepath.Dir(app.ExecutablePath)}, nil
}

func detectDarwinInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) {
	if app.ExecutablePath == "" {
		return updates.InstallRoot{}, errors.New("missing executable path")
	}

	current := filepath.Dir(app.ExecutablePath)
	for {
		if filepath.Ext(current) == ".app" {
			return updates.InstallRoot{Path: current}, nil
		}
		parent := filepath.Dir(current)
		if parent == current {
			return updates.InstallRoot{}, fmt.Errorf("unable to locate .app bundle from %s", app.ExecutablePath)
		}
		current = parent
	}
}
