package oxide

import (
	"archive/zip"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/nats-io/nats.go"
)

// GameServerRestarter abstracts the game server process manager so the installer
// can restart the server after extracting Oxide files.
type GameServerRestarter interface {
	Restart() error
}

// OxideInstaller handles downloading and extracting Oxide/uMod over a Rust server installation.
type OxideInstaller struct {
	nc         *nats.Conn
	licenseID  string
	installDir string
	gameServer GameServerRestarter
}

// NewOxideInstaller creates a new OxideInstaller instance.
func NewOxideInstaller(nc *nats.Conn, licenseID, installDir string, gs GameServerRestarter) *OxideInstaller {
	return &OxideInstaller{
		nc:         nc,
		licenseID:  licenseID,
		installDir: installDir,
		gameServer: gs,
	}
}

// githubRelease represents the relevant fields from the GitHub Releases API response.
type githubRelease struct {
	TagName string        `json:"tag_name"`
	Assets  []githubAsset `json:"assets"`
}

type githubAsset struct {
	Name               string `json:"name"`
	BrowserDownloadURL string `json:"browser_download_url"`
}

// Install performs the full Oxide installation pipeline:
// 1. Fetch latest release info from GitHub
// 2. Download the zip
// 3. Extract over {installDir}/server/
// 4. Restart the game server
func (o *OxideInstaller) Install() error {
	// Stage 1: Fetch latest release
	log.Printf("Oxide: fetching latest release for license %s", o.licenseID)
	o.publishStatus("fetching_release", 0, "Checking latest Oxide release...")

	release, err := o.fetchLatestRelease()
	if err != nil {
		o.publishStatus("failed", 0, "Failed to fetch Oxide release info", err.Error())
		return fmt.Errorf("fetch release failed: %w", err)
	}

	if len(release.Assets) == 0 {
		err := fmt.Errorf("no assets found in release %s", release.TagName)
		o.publishStatus("failed", 0, "No download assets in release", err.Error())
		return err
	}

	downloadURL := release.Assets[0].BrowserDownloadURL
	version := release.TagName
	log.Printf("Oxide: latest version is %s, download URL: %s", version, downloadURL)
	o.publishStatus("fetching_release", 100, fmt.Sprintf("Found Oxide %s", version))

	// Stage 2: Download zip
	log.Printf("Oxide: downloading %s", downloadURL)
	o.publishStatus("downloading", 0, fmt.Sprintf("Downloading Oxide %s...", version))

	tmpPath := filepath.Join(os.TempDir(), "oxide-latest.zip")
	if err := o.downloadFile(downloadURL, tmpPath); err != nil {
		o.publishStatus("failed", 0, "Failed to download Oxide", err.Error())
		return fmt.Errorf("download failed: %w", err)
	}
	defer os.Remove(tmpPath)

	log.Printf("Oxide: download complete")
	o.publishStatus("downloading", 100, "Download complete")

	// Stage 3: Extract over server directory
	serverDir := filepath.Join(o.installDir, "server")
	log.Printf("Oxide: extracting to %s", serverDir)
	o.publishStatus("installing", 0, "Extracting Oxide over server directory...")

	if err := o.extractZip(tmpPath, serverDir); err != nil {
		o.publishStatus("failed", 0, "Failed to extract Oxide", err.Error())
		return fmt.Errorf("extract failed: %w", err)
	}

	log.Printf("Oxide: extraction complete")
	o.publishStatus("installing", 100, "Oxide files extracted")

	// Stage 4: Restart server
	log.Printf("Oxide: restarting server")
	o.publishStatus("restarting", 0, "Restarting server to load Oxide...")

	if err := o.gameServer.Restart(); err != nil {
		o.publishStatus("failed", 0, "Server restart failed", err.Error())
		return fmt.Errorf("server restart failed: %w", err)
	}

	log.Printf("Oxide: server restarted, installation complete")
	o.publishStatus("complete", 100, fmt.Sprintf("Oxide %s installed successfully", version))

	return nil
}

// fetchLatestRelease queries the GitHub API for the latest Oxide.Rust release.
func (o *OxideInstaller) fetchLatestRelease() (*githubRelease, error) {
	client := &http.Client{Timeout: 30 * time.Second}

	resp, err := client.Get("https://api.github.com/repos/OxideMod/Oxide.Rust/releases/latest")
	if err != nil {
		return nil, fmt.Errorf("GitHub API request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
	}

	var release githubRelease
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		return nil, fmt.Errorf("failed to parse GitHub API response: %w", err)
	}

	return &release, nil
}

// downloadFile downloads a URL to a local file path.
func (o *OxideInstaller) downloadFile(url, destPath string) error {
	client := &http.Client{Timeout: 5 * time.Minute}

	resp, err := client.Get(url)
	if err != nil {
		return fmt.Errorf("HTTP GET failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download returned status %d", resp.StatusCode)
	}

	out, err := os.Create(destPath)
	if err != nil {
		return fmt.Errorf("failed to create file %s: %w", destPath, err)
	}
	defer out.Close()

	if _, err := io.Copy(out, resp.Body); err != nil {
		return fmt.Errorf("failed to write download: %w", err)
	}

	return nil
}

// extractZip extracts a zip file to a destination directory, overwriting existing files.
// This is used to overlay Oxide's DLLs over the Rust server's Managed directory
// and create the oxide/ folder structure.
func (o *OxideInstaller) extractZip(zipPath, destDir string) error {
	r, err := zip.OpenReader(zipPath)
	if err != nil {
		return fmt.Errorf("failed to open zip: %w", err)
	}
	defer r.Close()

	for _, f := range r.File {
		targetPath := filepath.Join(destDir, f.Name)

		// Security: prevent path traversal
		if !strings.HasPrefix(targetPath, filepath.Clean(destDir)+string(os.PathSeparator)) && targetPath != filepath.Clean(destDir) {
			log.Printf("Oxide: skipping potentially unsafe path: %s", f.Name)
			continue
		}

		if f.FileInfo().IsDir() {
			if err := os.MkdirAll(targetPath, 0755); err != nil {
				return fmt.Errorf("failed to create directory %s: %w", targetPath, err)
			}
			continue
		}

		// Ensure parent directory exists
		if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
			return fmt.Errorf("failed to create parent directory for %s: %w", targetPath, err)
		}

		outFile, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
		if err != nil {
			return fmt.Errorf("failed to create file %s: %w", targetPath, err)
		}

		rc, err := f.Open()
		if err != nil {
			outFile.Close()
			return fmt.Errorf("failed to open zip entry %s: %w", f.Name, err)
		}

		_, err = io.Copy(outFile, rc)
		rc.Close()
		outFile.Close()

		if err != nil {
			return fmt.Errorf("failed to extract %s: %w", f.Name, err)
		}
	}

	return nil
}

// publishStatus publishes an OxideStatus message to NATS. Publish errors are logged
// but do not fail the installation — losing a progress update is not fatal.
func (o *OxideInstaller) publishStatus(stage string, progress int, message string, errDetail ...string) {
	subject := fmt.Sprintf("corrosion.%s.oxide.status", o.licenseID)

	status := OxideStatus{
		Stage:     stage,
		Progress:  progress,
		Message:   message,
		Timestamp: time.Now().UTC().Format(time.RFC3339),
	}

	if len(errDetail) > 0 && errDetail[0] != "" {
		status.Error = errDetail[0]
	}

	data, err := json.Marshal(status)
	if err != nil {
		log.Printf("Failed to marshal oxide status: %v", err)
		return
	}

	if err := o.nc.Publish(subject, data); err != nil {
		log.Printf("Failed to publish oxide status to %s: %v", subject, err)
	}
}
