package clients

import (
	"context"
	"fmt"
	"strings"

	"git.linuxforward.com/byop/byop-engine/models"
	"github.com/docker/cli/cli/config/configfile"
	clitypes "github.com/docker/cli/cli/config/types"
	"github.com/moby/buildkit/client"
	"github.com/moby/buildkit/client/llb"
	"github.com/moby/buildkit/session"
	"github.com/moby/buildkit/session/auth/authprovider"
	"github.com/sirupsen/logrus"
	"github.com/tonistiigi/fsutil"
)

// BuildMachineClient defines the interface for a build machine client.
type BuildMachineClient interface {
	// BuildImage builds an image based on the job details and build options.
	// It returns the image ID or an equivalent identifier upon success.
	BuildImage(ctx context.Context, job models.BuildJob, dockerfilePath string, contextPath string, imageName string, imageTag string, noCache bool, buildArgs map[string]string) (string, error)

	// PushImage pushes a previously built image to a registry.
	// fullImageURI is the complete URI of the image to push (e.g., myregistry.com/user/image:tag).
	// registryURL is the base URL of the registry (e.g., "docker.io", "myregistry.com") used for auth.
	// username and password are the credentials for the registry.
	PushImage(ctx context.Context, job models.BuildJob, fullImageURI string, registryURL string, username string, password string) error

	// CheckImageExists checks if an image exists in the registry.
	// fullImageURI is the complete URI of the image to check (e.g., myregistry.com/user/image:tag).
	// registryURL is the base URL of the registry (e.g., "docker.io", "myregistry.com") used for auth.
	// username and password are the credentials for the registry.
	CheckImageExists(ctx context.Context, fullImageURI string, registryURL string, username string, password string) (bool, error)

	// Prune can be used to clean up build resources if necessary.
	Prune(ctx context.Context, job models.BuildJob) error // Assuming job might be needed for context, adjust if not.

	// Close releases any resources held by the client.
	Close() error
}

// BuildKitClient implements the BuildMachineClient interface using BuildKit.
type BuildKitClient struct {
	buildkitHost string
	entry        *logrus.Entry
}

// NewBuildKitClient creates a new BuildKitClient.
// buildkitHost is the address of the BuildKit daemon (e.g., "tcp://127.0.0.1:1234" or "docker-container://buildkitd")
func NewBuildKitClient(buildkitHost string) BuildMachineClient {
	return &BuildKitClient{
		buildkitHost: buildkitHost,
		entry:        logrus.WithField("component", "BuildKitClient"),
	}
}

// getClient ensures a BuildKit client is available.
// This is a helper to establish a connection on demand or use an existing one.
// For simplicity in this example, it creates a new client on each major operation,
// but in a production system, you might want to manage a persistent client.
func (bkc *BuildKitClient) getClient(ctx context.Context, job models.BuildJob) (*client.Client, error) {
	c, err := client.New(ctx, bkc.buildkitHost, nil)
	if err != nil {
		return nil, fmt.Errorf("job %d: failed to get BuildKit client: %w", job.ID, err)
	}
	return c, nil
}

// getClientForPush ensures a BuildKit client is available for push operations.
// This is a helper to establish a connection on demand or use an existing one.
func (bkc *BuildKitClient) getClientForPush(ctx context.Context, job models.BuildJob) (*client.Client, error) {
	c, err := client.New(ctx, bkc.buildkitHost, nil)
	if err != nil {
		return nil, fmt.Errorf("job %d: failed to get BuildKit client for push: %w", job.ID, err)
	}
	return c, nil
}

// FetchCode is not directly implemented as a separate step in typical BuildKit Dockerfile builds,
// as the Dockerfile's `COPY` or `ADD` instructions, or git sources, handle this.
// This method could be used to pre-fetch if needed, or this logic can be integrated into BuildImage.
// For this implementation, we assume the Dockerfile within the git repo will handle code fetching/access.
func (bkc *BuildKitClient) FetchCode(job models.BuildJob, sourceURL string, version string, targetDir string) error {
	bkc.entry.Infof("Job %d: FetchCode called (source: %s, version: %s). BuildKit handles this via Dockerfile context or git source.", job.ID, sourceURL, version)
	return nil
}

// BuildImage builds a Docker image using BuildKit.
// dockerfilePath is the path to the Dockerfile *within the git repository context*.
// contextPath is the sub-directory within the git repository to use as the build context.
func (bkc *BuildKitClient) BuildImage(ctx context.Context, job models.BuildJob, dockerfilePath string, contextPath string, imageName string, imageTag string, noCache bool, buildArgs map[string]string) (string, error) {
	bkc.entry.Infof("Job %d: Building image %s:%s. SourceURL: %s, BuildContext: %s, Dockerfile: %s, InitialContextPathArg: %s, InitialDockerfileArg: %s", job.ID, imageName, imageTag, job.SourceURL, job.BuildContext, job.Dockerfile, contextPath, dockerfilePath)

	buildkitClient, err := bkc.getClient(ctx, job)
	if err != nil {
		return "", fmt.Errorf("job %d: failed to get BuildKit client: %w", job.ID, err)
	}
	defer buildkitClient.Close()

	localImageName := fmt.Sprintf("%s:%s", imageName, imageTag)

	opts := client.SolveOpt{
		Exports: []client.ExportEntry{
			{
				Type: client.ExporterImage,
				Attrs: map[string]string{
					"name": localImageName,
				},
			},
		},
		LocalDirs:     map[string]string{},
		LocalMounts:   map[string]fsutil.FS{},
		FrontendAttrs: map[string]string{},
	}

	// Session authentication setup
	dockerCfgFile := configfile.New("") // Path to default Docker config file (~/.docker/config.json)
	// It's okay if this file doesn't exist or is empty; NewDockerAuthProvider handles it.
	// We wrap the ConfigFile in DockerAuthProviderConfig
	defaultAuthConfig := authprovider.DockerAuthProviderConfig{ConfigFile: dockerCfgFile}
	opts.Session = []session.Attachable{authprovider.NewDockerAuthProvider(defaultAuthConfig)}

	// Add specific auth for the target registry if provided in the job (for private base images, etc.)
	if job.RegistryURL != "" && job.RegistryUser != "" && job.RegistryPassword != "" {
		regAuthConfigValue := clitypes.AuthConfig{
			Username: job.RegistryUser,
			Password: job.RegistryPassword,
		}
		normalizedRegURL := job.RegistryURL
		// if job.RegistryURL == "docker.io" || job.RegistryURL == "" { // Docker Hub
		// 	normalizedRegURL = "https://index.docker.io/v1/"
		// } else if !strings.HasPrefix(job.RegistryURL, "http://") && !strings.HasPrefix(job.RegistryURL, "https://") {
		// 	normalizedRegURL = "http://" + job.RegistryURL
		// }

		specificAuthCfgFile := configfile.New("") // Create an empty config file object
		specificAuthCfgFile.AuthConfigs[normalizedRegURL] = regAuthConfigValue
		specificAuthConfig := authprovider.DockerAuthProviderConfig{ConfigFile: specificAuthCfgFile}
		opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(specificAuthConfig))
		bkc.entry.Infof("Job %d: Added specific auth for registry %s to build session.", job.ID, normalizedRegURL)
	}

	// LLB support has been removed. Use Dockerfile builds only.
	err = fmt.Errorf("job %d: LLB support has been removed from byop-engine. Please use Dockerfile-based builds", job.ID)
	bkc.entry.Error(err)
	return "", err
}

// PushImage pushes a Docker image using BuildKit.
func (bkc *BuildKitClient) PushImage(ctx context.Context, job models.BuildJob, fullImageURI string, registryURL string, username string, password string) error {
	// LLB support has been removed. Use Dockerfile builds only.
	err := fmt.Errorf("job %d: LLB support has been removed from byop-engine. Please use Dockerfile-based builds", job.ID)
	bkc.entry.Error(err)
	return err
}

// CheckImageExists checks if an image exists in the registry using Docker manifest API.
// This is a simplified implementation that uses BuildKit's registry capabilities.
func (bkc *BuildKitClient) CheckImageExists(ctx context.Context, fullImageURI string, registryURL string, username string, password string) (bool, error) {
	bkc.entry.Infof("Checking if image exists: %s", fullImageURI)

	// For now, we'll implement a simple approach using BuildKit's ability to reference images
	// We try to create a simple LLB definition that references the image and see if it resolves
	buildkitClient, err := bkc.getClient(ctx, models.BuildJob{ID: 0}) // Dummy job for connection
	if err != nil {
		return false, fmt.Errorf("failed to get BuildKit client for image check: %w", err)
	}
	defer buildkitClient.Close()

	// Create a simple LLB definition that references the image
	// This will fail if the image doesn't exist
	imageState := llb.Image(fullImageURI)
	def, err := imageState.Marshal(ctx)
	if err != nil {
		return false, fmt.Errorf("failed to marshal LLB definition for image check: %w", err)
	}

	opts := client.SolveOpt{
		Exports:       []client.ExportEntry{}, // No export, just check if image resolves
		LocalDirs:     map[string]string{},
		LocalMounts:   map[string]fsutil.FS{},
		FrontendAttrs: map[string]string{},
	}

	// Add authentication if provided
	if username != "" && password != "" {
		serverAddress := registryURL
		if registryURL == "docker.io" || registryURL == "" {
			serverAddress = "https://index.docker.io/v1/"
		} else if !strings.HasPrefix(registryURL, "http://") && !strings.HasPrefix(registryURL, "https://") {
			// For local registries (localhost, host.docker.internal), use HTTP
			if strings.Contains(registryURL, "localhost") || strings.Contains(registryURL, "host.docker.internal") {
				serverAddress = "http://" + registryURL
			} else {
				serverAddress = "https://" + registryURL
			}
		}

		cfgInMemory := &configfile.ConfigFile{
			AuthConfigs: make(map[string]clitypes.AuthConfig),
		}
		cfgInMemory.AuthConfigs[serverAddress] = clitypes.AuthConfig{
			Username: username,
			Password: password,
		}

		authProviderConfig := authprovider.DockerAuthProviderConfig{
			ConfigFile: cfgInMemory,
		}
		opts.Session = []session.Attachable{authprovider.NewDockerAuthProvider(authProviderConfig)}
	}

	// Try to solve the definition - this will fail if the image doesn't exist
	ch := make(chan *client.SolveStatus)

	// Drain the status channel in a goroutine
	go func() {
		for range ch {
			// Consume status updates but don't process them
		}
	}()

	_, err = buildkitClient.Solve(ctx, def, opts, ch)
	if err != nil {
		// Check if error indicates image not found
		errStr := err.Error()
		if strings.Contains(errStr, "not found") ||
			strings.Contains(errStr, "does not exist") ||
			strings.Contains(errStr, "manifest unknown") ||
			strings.Contains(errStr, "pull access denied") {
			bkc.entry.Infof("Image %s does not exist: %v", fullImageURI, err)
			return false, nil
		}
		// Other errors are actual failures
		return false, fmt.Errorf("failed to check image existence: %w", err)
	}

	bkc.entry.Infof("Image %s exists in registry", fullImageURI)
	return true, nil
}

// Prune is a no-op for BuildKitClient as it does not manage local resources.
func (bkc *BuildKitClient) Prune(ctx context.Context, job models.BuildJob) error {
	bkc.entry.Infof("Job %d: Prune called. BuildKit does not manage local resources directly.", job.ID)
	// In a real implementation, you might want to call a BuildKit prune operation if needed.
	return nil
}

// Close releases any resources held by the BuildKitClient.
func (bkc *BuildKitClient) Close() error {
	bkc.entry.Info("Closing BuildKitClient resources.")
	// BuildKit client does not hold persistent resources in this implementation.
	// If you had a persistent client, you would close it here.
	return nil
}
