// Package builders provides build implementations for different project types.
package builders

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/host-uk/core/pkg/build"
	"github.com/host-uk/core/pkg/io"
)

// CPPBuilder implements the Builder interface for C++ projects using CMake + Conan.
// It wraps the Makefile-based build system from the .core/build submodule.
type CPPBuilder struct{}

// NewCPPBuilder creates a new CPPBuilder instance.
func NewCPPBuilder() *CPPBuilder {
	return &CPPBuilder{}
}

// Name returns the builder's identifier.
func (b *CPPBuilder) Name() string {
	return "cpp"
}

// Detect checks if this builder can handle the project in the given directory.
func (b *CPPBuilder) Detect(fs io.Medium, dir string) (bool, error) {
	return build.IsCPPProject(fs, dir), nil
}

// Build compiles the C++ project using Make targets.
// The build flow is: make configure → make build → make package.
// Cross-compilation is handled via Conan profiles specified in .core/build.yaml.
func (b *CPPBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
	if cfg == nil {
		return nil, fmt.Errorf("builders.CPPBuilder.Build: config is nil")
	}

	// Validate make is available
	if err := b.validateMake(); err != nil {
		return nil, err
	}

	// For C++ projects, the Makefile handles everything.
	// We don't iterate per-target like Go — the Makefile's configure + build
	// produces binaries for the host platform, and cross-compilation uses
	// named Conan profiles (e.g., make gcc-linux-armv8).
	if len(targets) == 0 {
		// Default to host platform
		targets = []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
	}

	var artifacts []build.Artifact

	for _, target := range targets {
		built, err := b.buildTarget(ctx, cfg, target)
		if err != nil {
			return artifacts, fmt.Errorf("builders.CPPBuilder.Build: %w", err)
		}
		artifacts = append(artifacts, built...)
	}

	return artifacts, nil
}

// buildTarget compiles for a single target platform.
func (b *CPPBuilder) buildTarget(ctx context.Context, cfg *build.Config, target build.Target) ([]build.Artifact, error) {
	// Determine if this is a cross-compile or host build
	isHostBuild := target.OS == runtime.GOOS && target.Arch == runtime.GOARCH

	if isHostBuild {
		return b.buildHost(ctx, cfg, target)
	}

	return b.buildCross(ctx, cfg, target)
}

// buildHost runs the standard make configure → make build → make package flow.
func (b *CPPBuilder) buildHost(ctx context.Context, cfg *build.Config, target build.Target) ([]build.Artifact, error) {
	fmt.Printf("Building C++ project for %s/%s (host)\n", target.OS, target.Arch)

	// Step 1: Configure (runs conan install + cmake configure)
	if err := b.runMake(ctx, cfg.ProjectDir, "configure"); err != nil {
		return nil, fmt.Errorf("configure failed: %w", err)
	}

	// Step 2: Build
	if err := b.runMake(ctx, cfg.ProjectDir, "build"); err != nil {
		return nil, fmt.Errorf("build failed: %w", err)
	}

	// Step 3: Package
	if err := b.runMake(ctx, cfg.ProjectDir, "package"); err != nil {
		return nil, fmt.Errorf("package failed: %w", err)
	}

	// Discover artifacts from build/packages/
	return b.findArtifacts(cfg.FS, cfg.ProjectDir, target)
}

// buildCross runs a cross-compilation using a Conan profile name.
// The Makefile supports profile targets like: make gcc-linux-armv8
func (b *CPPBuilder) buildCross(ctx context.Context, cfg *build.Config, target build.Target) ([]build.Artifact, error) {
	// Map target to a Conan profile name
	profile := b.targetToProfile(target)
	if profile == "" {
		return nil, fmt.Errorf("no Conan profile mapped for target %s/%s", target.OS, target.Arch)
	}

	fmt.Printf("Building C++ project for %s/%s (cross: %s)\n", target.OS, target.Arch, profile)

	// The Makefile exposes each profile as a top-level target
	if err := b.runMake(ctx, cfg.ProjectDir, profile); err != nil {
		return nil, fmt.Errorf("cross-compile for %s failed: %w", profile, err)
	}

	return b.findArtifacts(cfg.FS, cfg.ProjectDir, target)
}

// runMake executes a make target in the project directory.
func (b *CPPBuilder) runMake(ctx context.Context, projectDir string, target string) error {
	cmd := exec.CommandContext(ctx, "make", target)
	cmd.Dir = projectDir
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Env = os.Environ()

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("make %s: %w", target, err)
	}
	return nil
}

// findArtifacts searches for built packages in build/packages/.
func (b *CPPBuilder) findArtifacts(fs io.Medium, projectDir string, target build.Target) ([]build.Artifact, error) {
	packagesDir := filepath.Join(projectDir, "build", "packages")

	if !fs.IsDir(packagesDir) {
		// Fall back to searching build/release/src/ for raw binaries
		return b.findBinaries(fs, projectDir, target)
	}

	entries, err := fs.List(packagesDir)
	if err != nil {
		return nil, fmt.Errorf("failed to list packages directory: %w", err)
	}

	var artifacts []build.Artifact
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		name := entry.Name()
		// Skip checksum files and hidden files
		if strings.HasSuffix(name, ".sha256") || strings.HasPrefix(name, ".") {
			continue
		}

		artifacts = append(artifacts, build.Artifact{
			Path: filepath.Join(packagesDir, name),
			OS:   target.OS,
			Arch: target.Arch,
		})
	}

	return artifacts, nil
}

// findBinaries searches for compiled binaries in build/release/src/.
func (b *CPPBuilder) findBinaries(fs io.Medium, projectDir string, target build.Target) ([]build.Artifact, error) {
	binDir := filepath.Join(projectDir, "build", "release", "src")

	if !fs.IsDir(binDir) {
		return nil, fmt.Errorf("no build output found in %s", binDir)
	}

	entries, err := fs.List(binDir)
	if err != nil {
		return nil, fmt.Errorf("failed to list build directory: %w", err)
	}

	var artifacts []build.Artifact
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		name := entry.Name()
		// Skip non-executable files (libraries, cmake files, etc.)
		if strings.HasSuffix(name, ".a") || strings.HasSuffix(name, ".o") ||
			strings.HasSuffix(name, ".cmake") || strings.HasPrefix(name, ".") {
			continue
		}

		fullPath := filepath.Join(binDir, name)

		// On Unix, check if file is executable
		if target.OS != "windows" {
			info, err := os.Stat(fullPath)
			if err != nil {
				continue
			}
			if info.Mode()&0111 == 0 {
				continue
			}
		}

		artifacts = append(artifacts, build.Artifact{
			Path: fullPath,
			OS:   target.OS,
			Arch: target.Arch,
		})
	}

	return artifacts, nil
}

// targetToProfile maps a build target to a Conan cross-compilation profile name.
// Profile names match those in .core/build/cmake/profiles/.
func (b *CPPBuilder) targetToProfile(target build.Target) string {
	key := target.OS + "/" + target.Arch
	profiles := map[string]string{
		"linux/amd64":    "gcc-linux-x86_64",
		"linux/x86_64":   "gcc-linux-x86_64",
		"linux/arm64":    "gcc-linux-armv8",
		"linux/armv8":    "gcc-linux-armv8",
		"darwin/arm64":   "apple-clang-armv8",
		"darwin/armv8":   "apple-clang-armv8",
		"darwin/amd64":   "apple-clang-x86_64",
		"darwin/x86_64":  "apple-clang-x86_64",
		"windows/amd64":  "msvc-194-x86_64",
		"windows/x86_64": "msvc-194-x86_64",
	}

	return profiles[key]
}

// validateMake checks if make is available.
func (b *CPPBuilder) validateMake() error {
	if _, err := exec.LookPath("make"); err != nil {
		return fmt.Errorf("cpp: make not found. Install build-essential (Linux) or Xcode Command Line Tools (macOS)")
	}
	return nil
}

// Ensure CPPBuilder implements the Builder interface.
var _ build.Builder = (*CPPBuilder)(nil)
