// This file is the install orchestrator (#162): it ties together the pieces the
// earlier issues built — fetch (#156), classify (#157), the per-type
// distributors (#158-#161), and the lockfile (#154) — into the single
// `pigo install npm:<name>` flow.
//
// The flow is: parse the reference, fetch+extract the package to a temp dir,
// classify it into one or more pi types, distribute each type to its target
// directory, then record everything laid down in the lockfile so list/uninstall
// /update can act on it. The temp dir is always cleaned up.
package pkgmgr

import (
	"fmt"
	"io"
	"sort"
	"strings"
)

// InstallResult reports what an install did, for the CLI to print.
type InstallResult struct {
	Name    string
	Version string
	Types   []PackageType
	// Files is every path laid down on disk across all distributed types.
	Files []string
}

// Install fetches, classifies, and distributes the package named by rawRef
// (e.g. "npm:pi-mcp-adapter"), then records it in the lockfile at lockfilePath.
// Progress is written to logw when non-nil. It returns a summary of the install.
//
// The install is type-driven: a package classified as several types (e.g.
// extension+skill) is distributed to each corresponding directory. If any
// distribution step fails, the error is returned; already-written files are left
// for the caller/uninstall to reconcile via a re-run (distribution is
// idempotent — each distributor clears its own stale target first).
func Install(rawRef, lockfilePath string, logw io.Writer) (InstallResult, error) {
	logf := func(format string, a ...any) {
		if logw != nil {
			fmt.Fprintf(logw, format, a...)
		}
	}

	ref, err := ParsePackageRef(rawRef)
	if err != nil {
		return InstallResult{}, err
	}

	logf("Fetching %s ...\n", ref.String())
	fetched, err := Fetch(ref)
	if err != nil {
		return InstallResult{}, err
	}
	defer fetched.Cleanup()

	return installFetched(fetched.Dir, ref, lockfilePath, logf)
}

// installFetched runs the post-fetch half of an install: classify the already
// extracted package at pkgDir, distribute each type, and record the result in
// the lockfile. It is shared by Install (#162) and Update (#164) so both go
// through the same classify→distribute→lockfile path.
func installFetched(pkgDir string, ref PackageRef, lockfilePath string, logf func(string, ...any)) (InstallResult, error) {
	name, version, types, err := Classify(pkgDir)
	if err != nil {
		return InstallResult{}, err
	}
	logf("Installing %s@%s (%s)\n", name, version, joinTypes(types))

	var files []string
	for _, t := range types {
		created, derr := distribute(t, pkgDir, name)
		if derr != nil {
			return InstallResult{}, derr
		}
		files = append(files, created...)
	}
	sort.Strings(files)

	lf, err := Load(lockfilePath)
	if err != nil {
		return InstallResult{}, err
	}
	lf.Set(InstalledPackage{
		Name:    name,
		Source:  ref.String(),
		Version: version,
		Types:   types,
		Files:   files,
	})
	if err := lf.Save(); err != nil {
		return InstallResult{}, err
	}

	return InstallResult{Name: name, Version: version, Types: types, Files: files}, nil
}

// distribute routes one package type to its distributor. An unknown type is an
// error (Classify should never produce one, but guard anyway).
func distribute(t PackageType, pkgDir, name string) ([]string, error) {
	switch t {
	case TypeExtension:
		return DistributeExtension(pkgDir, name)
	case TypeSkill:
		return DistributeSkill(pkgDir, name)
	case TypePrompt:
		return DistributePrompt(pkgDir, name)
	case TypeTheme:
		return DistributeTheme(pkgDir, name)
	default:
		return nil, fmt.Errorf("pkgmgr: cannot distribute unknown type %q", t)
	}
}

// joinTypes renders a type slice as a comma-separated string for logging.
func joinTypes(types []PackageType) string {
	parts := make([]string, len(types))
	for i, t := range types {
		parts[i] = string(t)
	}
	return strings.Join(parts, ", ")
}
