package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
)

const version = "0.4.0-boredos"

var (
	home        = os.Getenv("HOME")
	bin         = filepath.Join(home, ".local", "bin")
	src         = ".mol/src"
	db          = ".mol/repo"
	stage       = ".mol/stage"
	manifestDir = ".mol/manifests"
	reposFile   = ".mol/repos"
	depCache    = ".mol/depcache.json"
	defaultRepo = envOr("MOL_REPO", "https://git.sr.ht/~chersbobers/mol-pkgrepo/blob/master/PACKAGES?raw=true")
	goos        = runtime.GOOS
)

func init() {
	if goos == "windows" {
		bin = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local", "mol", "bin")
	}
}

func envOr(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

const (
	reset  = "\033[0m"
	bold   = "\033[1m"
	dim    = "\033[2m"
	green  = "\033[32m"
	yellow = "\033[33m"
	cyan   = "\033[36m"
	red    = "\033[31m"
)

func uiInfo(msg string)    { fmt.Printf("%s  →%s %s\n", cyan, reset, msg) }
func uiOk(msg string)      { fmt.Printf("%s  ✓%s %s%s%s\n", green, reset, bold, msg, reset) }
func uiWarn(msg string)    { fmt.Printf("%s  ⚠%s %s\n", yellow, reset, msg) }
func uiErr(msg string)     { fmt.Fprintf(os.Stderr, "%s  ✗%s %s%s%s\n", red, reset, bold, msg, reset) }
func uiDim(msg string)     { fmt.Printf("%s    %s%s\n", dim, msg, reset) }
func uiSection(msg string) { fmt.Printf("\n%s%s▸ %s%s\n", bold, cyan, msg, reset) }

func uiHeader() {
	fmt.Printf("%s%smol%s %sv%s · GPLv3%s\n", bold, cyan, reset, dim, version, reset)
	fmt.Printf("%s%s%s\n", dim, strings.Repeat("─", 42), reset)
}

func uiProgress(label string, total int, fn func(int)) {
	for i := 0; i < total; i++ {
		pct := ((i + 1) * 100) / total
		done := (30 * pct) / 100
		bar := strings.Repeat("█", done) + strings.Repeat("░", 30-done)
		fmt.Printf("\r  %s%s%s %d%% %s%s%s   ", cyan, bar, reset, pct, dim, label, reset)
		fn(i)
	}
	fmt.Println()
}

func run(cmd string, args []string, dir string) bool {
	c := exec.Command(cmd, args...)
	c.Dir = dir
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr
	return c.Run() == nil
}

func stageBegin(name string) string {
	path := filepath.Join(stage, name)
	os.RemoveAll(path)
	os.MkdirAll(path, 0755)
	return path
}

func stageCommit(name, stagedPath string) {
	manifestPath := filepath.Join(manifestDir, name+".list")
	os.MkdirAll(manifestDir, 0755)
	var written []string

	filepath.Walk(stagedPath, func(path string, info os.FileInfo, err error) error {
		if err != nil || info.IsDir() {
			return nil
		}
		rel, _ := filepath.Rel(stagedPath, path)
		dest := filepath.Join("/", rel)
		os.MkdirAll(filepath.Dir(dest), 0755)
		copyFile(path, dest)
		written = append(written, dest)
		return nil
	})

	os.WriteFile(manifestPath, []byte(strings.Join(written, "\n")), 0644)
	os.RemoveAll(stagedPath)
	uiOk(fmt.Sprintf("committed %d file(s) for %s", len(written), name))
}

func stageRollback(name string) {
	manifestPath := filepath.Join(manifestDir, name+".list")
	if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
		return
	}
	uiWarn("rolling back " + name + "…")
	f, _ := os.Open(manifestPath)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		path := strings.TrimSpace(scanner.Text())
		if path == "" {
			continue
		}
		os.Remove(path)
		uiDim("removed " + path)
	}
	f.Close()
	os.Remove(manifestPath)
	uiOk("rollback complete for " + name)
}

func hasManifest(name string) bool {
	_, err := os.Stat(filepath.Join(manifestDir, name+".list"))
	return err == nil
}

func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()
	_, err = io.Copy(out, in)
	return err
}

type graph map[string][]string

func loadGraph() graph {
	g := make(graph)
	data, err := os.ReadFile(depCache)
	if err != nil {
		return g
	}
	json.Unmarshal(data, &g)
	return g
}

func saveGraph(g graph) {
	os.MkdirAll(filepath.Dir(depCache), 0755)
	data, _ := json.Marshal(g)
	os.WriteFile(depCache, data, 0644)
}

func resolveGraph(root string, g graph) []string {
	var order []string
	visited := make(map[string]bool)
	var visit func(string)
	visit = func(name string) {
		if visited[name] {
			return
		}
		visited[name] = true
		for _, dep := range g[name] {
			visit(dep)
		}
		order = append(order, name)
	}
	visit(root)
	return order
}

func loadRepos() []string {
	repos := []string{defaultRepo}
	f, err := os.Open(reposFile)
	if err != nil {
		return repos
	}
	defer f.Close()
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		l := strings.TrimSpace(scanner.Text())
		if l == "" || strings.HasPrefix(l, "#") {
			continue
		}
		found := false
		for _, r := range repos {
			if r == l {
				found = true
				break
			}
		}
		if !found {
			repos = append(repos, l)
		}
	}
	return repos
}

func addRepo(url string) {
	os.MkdirAll(filepath.Dir(reposFile), 0755)
	f, _ := os.OpenFile(reposFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	defer f.Close()
	fmt.Fprintln(f, url)
	uiOk("added repo: " + url)
}

func lookup(t string) (string, string) {
	if strings.Contains(t, "://") {
		return t, ""
	}
	f, err := os.Open(db)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s not found — run: mol sync\n", t)
		os.Exit(1)
	}
	defer f.Close()
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		l := strings.TrimSpace(scanner.Text())
		if l == "" || strings.HasPrefix(l, "#") {
			continue
		}
		parts := strings.Fields(l)
		if len(parts) < 2 || parts[0] != t {
			continue
		}
		binURL := ""
		for _, p := range parts {
			if strings.HasPrefix(p, "B=") {
				binURL = p[2:]
			}
		}
		return parts[1], binURL
	}
	fmt.Fprintf(os.Stderr, "%s not found — run: mol sync\n", t)
	os.Exit(1)
	return "", ""
}

func installed(name string) bool {
	if hasManifest(name) {
		return true
	}
	if _, err := os.Stat(filepath.Join(src, name)); err == nil {
		return true
	}
	if _, err := os.Stat(filepath.Join(bin, name)); err == nil {
		return true
	}
	return false
}

func linkBins(path string) {
	os.MkdirAll(bin, 0755)
	candidates := []string{
		filepath.Join(path, "bin"),
		filepath.Join(path, "target", "release"),
		path,
	}
	for _, d := range candidates {
		if _, err := os.Stat(d); os.IsNotExist(err) {
			continue
		}
		entries, _ := os.ReadDir(d)
		for _, e := range entries {
			if e.IsDir() || strings.Contains(e.Name(), ".") {
				continue
			}
			full := filepath.Join(d, e.Name())
			info, _ := e.Info()
			if info.Mode()&0111 == 0 {
				continue
			}
			dest := filepath.Join(bin, e.Name())
			copyFile(full, dest)
			os.Chmod(dest, 0755)
			uiInfo("linked → " + dest)
		}
	}
}

func parseMolpkg(path string) ([]string, []string) {
	var deps, build []string
	file := filepath.Join(path, "mol.pkg")
	f, err := os.Open(file)
	if err != nil {
		return deps, build
	}
	defer f.Close()

	inBuild, inOs, inInstall := false, false, false
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		l := strings.TrimSpace(scanner.Text())
		switch {
		case strings.Contains(l, "(deps"):
			for _, m := range extractQuoted(l) {
				deps = append(deps, m)
			}
		case strings.Contains(l, "(build"):
			inBuild = true
		case strings.Contains(l, "(install"):
			inBuild, inInstall = false, true
		case inBuild && strings.Contains(l, "("+goos):
			inOs = true
		case inBuild && inOs && strings.HasPrefix(l, "(run"):
			parts := extractQuoted(l)
			if len(parts) > 0 {
				build = append(build, strings.Join(parts, " "))
			}
		case l == ")" && inOs:
			inOs = false
		case l == ")" && inBuild:
			inBuild = false
		case inInstall && strings.Contains(l, "(bin"):
			quoted := extractQuoted(l)
			if len(quoted) == 0 {
				continue
			}
			name := quoted[0]
			srcPath := filepath.Join(path, name)
			if goos == "windows" {
				if _, err := os.Stat(srcPath); os.IsNotExist(err) {
					srcPath = srcPath + ".exe"
				}
			}
			if _, err := os.Stat(srcPath); err == nil {
				dest := filepath.Join(bin, filepath.Base(srcPath))
				copyFile(srcPath, dest)
				os.Chmod(dest, 0755)
				uiInfo("linked → " + dest)
			}
		}
	}
	return deps, build
}

func extractQuoted(s string) []string {
	var results []string
	for {
		start := strings.Index(s, `"`)
		if start == -1 {
			break
		}
		end := strings.Index(s[start+1:], `"`)
		if end == -1 {
			break
		}
		results = append(results, s[start+1:start+1+end])
		s = s[start+1+end+1:]
	}
	return results
}

func install(name string, fromSrc, fast bool) {
	if installed(name) {
		return
	}
	uiSection("installing " + name)

	url, binURL := lookup(name)

	if (fast || !fromSrc) && binURL != "" {
		uiInfo("fetching prebuilt binary…")
		resp, err := http.Get(binURL)
		if err != nil || resp.StatusCode != 200 {
			fmt.Fprintln(os.Stderr, "download failed")
			os.Exit(1)
		}
		defer resp.Body.Close()
		os.MkdirAll(bin, 0755)
		dest := filepath.Join(bin, filepath.Base(binURL))
		data, _ := io.ReadAll(resp.Body)
		os.WriteFile(dest, data, 0755)
		uiOk(name + " installed (binary)")
		return
	}

	n := strings.TrimSuffix(strings.TrimSuffix(filepath.Base(url), ".git"), ".hg")
	clonePath := filepath.Join(src, n)
	vcs := "git"
	if strings.Contains(url, "hg") {
		vcs = "hg"
	}
	uiInfo("cloning " + url + "…")
	if !run(vcs, []string{"clone", url, clonePath}, "") {
		fmt.Fprintln(os.Stderr, "clone failed")
		os.Exit(1)
	}

	deps, buildCmds := parseMolpkg(clonePath)

	g := loadGraph()
	g[name] = deps
	saveGraph(g)

	order := resolveGraph(name, g)
	for _, dep := range order {
		if dep != name && !installed(dep) {
			uiInfo("dependency: " + dep)
			install(dep, fromSrc, fast)
		}
	}

	if len(buildCmds) == 0 {
		fmt.Fprintf(os.Stderr, "no build steps for %s in %s\n", goos, n)
		os.Exit(1)
	}

	staged := stageBegin(name)
	uiSection("building " + name)
	failed := false
	uiProgress("compiling", len(buildCmds), func(i int) {
		parts := strings.Fields(buildCmds[i])
		if !run(parts[0], parts[1:], clonePath) {
			failed = true
		}
	})
	if failed {
		stageRollback(name)
		fmt.Fprintln(os.Stderr, "build failed")
		os.Exit(1)
	}
	linkBins(clonePath)
	stageCommit(name, staged)
	uiOk(name + " installed from source")
}

func removePkg(name string) {
	uiSection("removing " + name)
	if hasManifest(name) {
		stageRollback(name)
	}
	os.RemoveAll(filepath.Join(src, name))
	os.Remove(filepath.Join(bin, name))
	os.Remove(filepath.Join(bin, name+".exe"))
	uiOk(name + " removed")
}

func updatePkg(name string, fast bool) {
	clonePath := filepath.Join(src, name)
	if fast {
		uiInfo("fast update: reinstalling binary for " + name + "…")
		removePkg(name)
		install(name, false, true)
		return
	}
	uiSection("updating " + name)
	if _, err := os.Stat(clonePath); os.IsNotExist(err) {
		fmt.Fprintln(os.Stderr, name+" not found, install first")
		os.Exit(1)
	}
	vcs := "git"
	if _, err := os.Stat(filepath.Join(clonePath, ".hg")); err == nil {
		vcs = "hg"
	}
	args := []string{"pull"}
	if vcs == "hg" {
		args = []string{"pull", "-u"}
	}
	if !run(vcs, args, clonePath) {
		fmt.Fprintln(os.Stderr, "pull failed")
		os.Exit(1)
	}
	_, buildCmds := parseMolpkg(clonePath)
	staged := stageBegin(name)
	for _, cmd := range buildCmds {
		parts := strings.Fields(cmd)
		if !run(parts[0], parts[1:], clonePath) {
			stageRollback(name)
			fmt.Fprintln(os.Stderr, "build failed: "+cmd)
			os.Exit(1)
		}
	}
	linkBins(clonePath)
	stageCommit(name, staged)
	uiOk(name + " updated")
}

func syncAll() {
	uiSection("syncing repos")
	repos := loadRepos()
	os.MkdirAll(filepath.Dir(db), 0755)
	var combined strings.Builder
	for i, repo := range repos {
		uiInfo(fmt.Sprintf("(%d/%d) %s", i+1, len(repos), repo))
		resp, err := http.Get(repo)
		if err != nil || resp.StatusCode != 200 {
			uiWarn(fmt.Sprintf("skipped: %s", repo))
			continue
		}
		data, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		combined.Write(data)
		combined.WriteString("\n")
	}
	os.WriteFile(db, []byte(combined.String()), 0644)
	uiOk(fmt.Sprintf("synced %d repo(s)", len(repos)))
}

func bootstrap(recipePath string) {
	uiSection("bootstrapping BoredOS from " + recipePath)
	if _, err := os.Stat(recipePath); os.IsNotExist(err) {
		fmt.Fprintln(os.Stderr, "recipe not found: "+recipePath)
		os.Exit(1)
	}
	deps, buildCmds := parseMolpkg(filepath.Dir(recipePath))
	uiInfo("installing bootstrap deps…")
	for _, d := range deps {
		install(d, false, false)
	}
	uiInfo("running bootstrap build steps…")
	for _, cmd := range buildCmds {
		parts := strings.Fields(cmd)
		uiDim("$ " + cmd)
		if !run(parts[0], parts[1:], "") {
			fmt.Fprintln(os.Stderr, "bootstrap step failed: "+cmd)
			os.Exit(1)
		}
	}
	uiOk("bootstrap complete")
}

func contains(args []string, flag string) bool {
	for _, a := range args {
		if a == flag {
			return true
		}
	}
	return false
}

func main() {
	os.MkdirAll(src, 0755)
	os.MkdirAll(stage, 0755)
	os.MkdirAll(manifestDir, 0755)
	os.MkdirAll(filepath.Dir(db), 0755)

	args := os.Args[1:]
	cmd := ""
	if len(args) > 0 {
		cmd = args[0]
	}

	switch cmd {
	case "install":
		if len(args) < 2 {
			fmt.Fprintln(os.Stderr, "usage: mol install <name> [--source] [-f]")
			os.Exit(1)
		}
		install(args[1], contains(args, "--source"), contains(args, "-f"))

	case "remove":
		if len(args) < 2 {
			fmt.Fprintln(os.Stderr, "usage: mol remove <name>")
			os.Exit(1)
		}
		removePkg(args[1])

	case "update":
		if len(args) < 2 {
			fmt.Fprintln(os.Stderr, "usage: mol update <name> [-f]")
			os.Exit(1)
		}
		updatePkg(args[1], contains(args, "-f"))

	case "sync":
		syncAll()

	case "repo":
		if len(args) < 2 {
			fmt.Fprintln(os.Stderr, "usage: mol repo <add|list>")
			os.Exit(1)
		}
		switch args[1] {
		case "add":
			if len(args) < 3 {
				fmt.Fprintln(os.Stderr, "usage: mol repo add <url>")
				os.Exit(1)
			}
			addRepo(args[2])
		case "list":
			for i, r := range loadRepos() {
				fmt.Printf("  %d. %s\n", i+1, r)
			}
		}

	case "bootstrap":
		if len(args) < 2 {
			fmt.Fprintln(os.Stderr, "usage: mol bootstrap <mol.pkg path>")
			os.Exit(1)
		}
		bootstrap(args[1])

	case "-l":
		if _, err := os.Stat(db); os.IsNotExist(err) {
			fmt.Fprintln(os.Stderr, "no repo — run: mol sync")
			os.Exit(1)
		}
		f, _ := os.Open(db)
		defer f.Close()
		scanner := bufio.NewScanner(f)
		for scanner.Scan() {
			l := strings.TrimSpace(scanner.Text())
			if l == "" || strings.HasPrefix(l, "#") {
				continue
			}
			fmt.Println(strings.Fields(l)[0])
		}

	case "--version", "-v":
		uiHeader()
		fmt.Printf("  OS      : %s\n", goos)
		fmt.Printf("  arch    : %s\n", runtime.GOARCH)

	default:
		uiHeader()
		fmt.Printf(`
  %sUsage:%s mol <command> [options]

  %sPackage commands:%s
    install <name> [--source] [-f]   install a package
    remove  <name>                   remove a package
    update  <name> [-f]              pull, rebuild, re-link
    sync                             fetch all configured repos

  %sRepo commands:%s
    repo add  <url>                  register an external repo
    repo list                        show configured repos

  %sInfo:%s
    -l                               list available packages
    --version                        show version and platform info

`, bold, reset, cyan, reset, cyan, reset, cyan, reset, cyan, reset)
	}
}