package devcontainer

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"zlh-agent/internal/provcommon"
	"zlh-agent/internal/state"
)

/*
   Dev container provisioning helpers.

   This file is intentionally small and boring.
   It exists to support idempotency and shared paths
   across dev container runtimes.
*/

const (
	// MarkerDir is where devcontainer state is stored.
	MarkerDir = "/opt/zlh/.zlh"

	// WorkspaceDir is the root working directory exposed to dev containers.
	WorkspaceDir = "/home/dev/workspace"

	// Dev user environment
	DevUser = "dev"
	DevHome = "/home/dev"

	// CatalogRelativePath is the artifact-server path to the dev runtime catalog.
	CatalogRelativePath = "devcontainer/_catalog.json"

	// RuntimeRoot is where versioned dev runtimes are installed.
	RuntimeRoot = "/opt/zlh/runtimes"

	// ReadyMarker is written after a dev container is fully provisioned.
	ReadyMarker = "devcontainer_ready.json"
)

type Catalog struct {
	Runtimes []CatalogRuntime `json:"runtimes"`
}

type CatalogRuntime struct {
	ID       string   `json:"id"`
	Versions []string `json:"versions"`
}

type ReadyInfo struct {
	Runtime string `json:"runtime"`
	ReadyAt string `json:"ready_at"`
}

// ReadyMarkerPath returns the absolute path to the ready marker file.
func ReadyMarkerPath() string {
	return filepath.Join(MarkerDir, ReadyMarker)
}

// IsProvisioned returns true if the dev container has already been installed.
func IsProvisioned() bool {
	_, err := os.Stat(ReadyMarkerPath())
	return err == nil
}

func ReadReadyMarker() (*ReadyInfo, error) {
	raw, err := os.ReadFile(ReadyMarkerPath())
	if err != nil {
		return nil, err
	}

	var info ReadyInfo
	if err := json.Unmarshal(raw, &info); err != nil {
		return nil, err
	}
	return &info, nil
}

// WriteReadyMarker records successful dev container provisioning.
// This should be called by runtime installers AFTER all install steps succeed.
func WriteReadyMarker(runtime string) error {

	if err := os.MkdirAll(MarkerDir, 0755); err != nil {
		return err
	}

	data := map[string]any{
		"runtime":  runtime,
		"ready_at": time.Now().UTC().Format(time.RFC3339),
	}

	raw, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(ReadyMarkerPath(), raw, 0644)
}

// EnsureWorkspace makes sure the dev workspace root exists before shell/filesystem access.
func EnsureWorkspace() error {
	return os.MkdirAll(WorkspaceDir, 0755)
}

func LoadCatalog() (*Catalog, error) {
	url := provcommon.BuildArtifactURL(CatalogRelativePath)
	log.Printf("[provision] action=load_catalog url=%s", url)
	resp, err := (&http.Client{Timeout: 15 * time.Second}).Get(url)
	if err != nil {
		return nil, fmt.Errorf("fetch dev runtime catalog: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("fetch dev runtime catalog: status %d", resp.StatusCode)
	}

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read dev runtime catalog: %w", err)
	}

	var catalog Catalog
	if err := json.Unmarshal(raw, &catalog); err != nil {
		return nil, fmt.Errorf("parse dev runtime catalog: %w", err)
	}
	log.Printf("[provision] action=load_catalog status=ok runtimes=%d", len(catalog.Runtimes))
	return &catalog, nil
}

func ValidateRuntimeSelection(cfg state.Config) error {
	catalog, err := LoadCatalog()
	if err != nil {
		return err
	}

	runtimeID := strings.ToLower(strings.TrimSpace(cfg.Runtime))
	version := strings.TrimSpace(cfg.Version)
	if runtimeID == "" {
		return fmt.Errorf("dev runtime missing")
	}
	if version == "" {
		return fmt.Errorf("dev runtime version missing")
	}

	for _, runtime := range catalog.Runtimes {
		if strings.EqualFold(runtime.ID, runtimeID) {
			for _, candidate := range runtime.Versions {
				if candidate == version {
					log.Printf("[provision] action=validate_runtime runtime=%s version=%s status=ok", runtimeID, version)
					return nil
				}
			}
			return fmt.Errorf("unsupported %s version: %s", runtimeID, version)
		}
	}

	return fmt.Errorf("unsupported dev runtime: %s", runtimeID)
}

func RuntimeInstallDir(runtimeName, version string) string {
	return filepath.Join(RuntimeRoot, strings.ToLower(strings.TrimSpace(runtimeName)), strings.TrimSpace(version))
}

func RuntimeInstalled(runtimeName, version string) bool {
	info, err := os.Stat(RuntimeInstallDir(runtimeName, version))
	return err == nil && info.IsDir()
}

func RuntimeMarker(runtimeName, version string) string {
	return fmt.Sprintf("devcontainer-%s-%s", strings.ToLower(strings.TrimSpace(runtimeName)), strings.ReplaceAll(strings.TrimSpace(version), "/", "_"))
}

func EnsureDevUserEnvironment() error {
	if _, err := user.Lookup(DevUser); err != nil {
		if err := exec.Command("useradd", "-m", "-s", "/bin/bash", DevUser).Run(); err != nil {
			return fmt.Errorf("create dev user: %w", err)
		}
	}

	if err := os.MkdirAll(WorkspaceDir, 0755); err != nil {
		return fmt.Errorf("create workspace: %w", err)
	}

	u, err := user.Lookup(DevUser)
	if err != nil {
		return fmt.Errorf("lookup dev user: %w", err)
	}
	uid, err := strconv.Atoi(u.Uid)
	if err != nil {
		return fmt.Errorf("parse dev uid: %w", err)
	}
	gid, err := strconv.Atoi(u.Gid)
	if err != nil {
		return fmt.Errorf("parse dev gid: %w", err)
	}

	if err := filepath.Walk(DevHome, func(path string, info os.FileInfo, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		return os.Chown(path, uid, gid)
	}); err != nil {
		return fmt.Errorf("chown dev home: %w", err)
	}

	return nil
}
