package static

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/alexei/tinyforge/internal/staticsite/deno"
)

// prepareDenoBuild assembles the Deno container build context: api/
// becomes the routes directory, every other file lands under public/,
// and a generated router.ts + Dockerfile finishes the context.
//
// Ported verbatim from internal/staticsite/manager.go so the legacy and
// plugin-native paths produce byte-identical containers from the same
// repo content. Fall back to copy when os.Rename hits EXDEV (cross-
// device) — the build dir and context dir live under the same temp
// root in production but tests may straddle filesystems.
func prepareDenoBuild(srcDir, contextDir string) error {
	apiSrc := filepath.Join(srcDir, "api")
	apiDst := filepath.Join(contextDir, "api")
	if err := os.Rename(apiSrc, apiDst); err != nil {
		return fmt.Errorf("move api dir: %w", err)
	}

	publicDir := filepath.Join(contextDir, "public")
	if err := os.Rename(srcDir, publicDir); err != nil {
		// EXDEV (cross-device) — fall back to copy.
		if err := copyDir(srcDir, publicDir); err != nil {
			return fmt.Errorf("copy public dir: %w", err)
		}
	}

	routes, err := deno.ScanRoutes(apiDst)
	if err != nil {
		return fmt.Errorf("scan routes: %w", err)
	}

	routerSrc, err := deno.GenerateRouter(routes)
	if err != nil {
		return fmt.Errorf("generate router: %w", err)
	}

	if err := os.WriteFile(filepath.Join(contextDir, "router.ts"), []byte(routerSrc), 0o644); err != nil {
		return fmt.Errorf("write router.ts: %w", err)
	}

	dockerfile := deno.GenerateDockerfile()
	if err := os.WriteFile(filepath.Join(contextDir, "Dockerfile"), []byte(dockerfile), 0o644); err != nil {
		return fmt.Errorf("write Dockerfile: %w", err)
	}

	return nil
}

// prepareStaticBuild assembles the nginx container build context: copy
// every file in srcDir into contextDir and emit the static Dockerfile.
func prepareStaticBuild(srcDir, contextDir string) error {
	if err := copyDir(srcDir, contextDir); err != nil {
		return fmt.Errorf("copy files: %w", err)
	}

	dockerfile := deno.GenerateStaticDockerfile()
	if err := os.WriteFile(filepath.Join(contextDir, "Dockerfile"), []byte(dockerfile), 0o644); err != nil {
		return fmt.Errorf("write Dockerfile: %w", err)
	}

	return nil
}

// copyDir recursively copies a directory tree, preserving file modes.
//
// Defense in depth against attacker-controlled provider responses:
// uses Lstat (via filepath.WalkDir + d.Type()) so symlinks are
// rejected outright instead of dereferenced — a hostile repo could
// otherwise drop a symlink that copyDir would chase outside the
// build context (or through a dangling chain at build-time). Also
// rejects entries whose resolved destination escapes dst.
func copyDir(src, dst string) error {
	cleanSrc := filepath.Clean(src)
	cleanDst := filepath.Clean(dst)
	return filepath.WalkDir(cleanSrc, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}

		// Reject anything that isn't a regular file or directory.
		// In particular: symlinks, devices, sockets, named pipes —
		// none of which belong in a static-site build context.
		if !d.IsDir() && !d.Type().IsRegular() {
			return fmt.Errorf("refusing to copy non-regular entry %s (mode %s)", path, d.Type())
		}

		relPath, err := filepath.Rel(cleanSrc, path)
		if err != nil {
			return err
		}
		dstPath := filepath.Join(cleanDst, relPath)

		// Belt-and-braces: filepath.Rel + Join shouldn't ever produce
		// an escaping path, but if a future refactor introduces one
		// (e.g. allowing non-cleaned roots), surface it loudly here
		// rather than silently writing outside the build context.
		if !strings.HasPrefix(dstPath, cleanDst+string(os.PathSeparator)) && dstPath != cleanDst {
			return fmt.Errorf("refusing to write outside build context: %s", dstPath)
		}

		if d.IsDir() {
			return os.MkdirAll(dstPath, 0o755)
		}

		info, err := d.Info()
		if err != nil {
			return err
		}
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		return os.WriteFile(dstPath, data, info.Mode())
	})
}

// verifyDownloadInsideRoot walks root and rejects any entry that has
// resolved to a symlink or whose resolved path escapes root. Used as a
// post-download guard against attacker-controlled tree responses from
// the Git provider — even though the providers themselves should
// never write outside their destination, this is the second line of
// defense and runs before the build context copy so a malicious
// download is contained.
func verifyDownloadInsideRoot(root string) error {
	cleanRoot := filepath.Clean(root)
	return filepath.WalkDir(cleanRoot, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if !d.IsDir() && !d.Type().IsRegular() {
			return fmt.Errorf("downloaded tree contains non-regular entry %s (mode %s)",
				path, d.Type())
		}
		clean := filepath.Clean(path)
		if clean != cleanRoot && !strings.HasPrefix(clean, cleanRoot+string(os.PathSeparator)) {
			return fmt.Errorf("downloaded entry escapes root: %s", clean)
		}
		return nil
	})
}
