package service

import (
	"context"
	"database/sql"
	"encoding/base64"
	"fmt"
	"path/filepath"
	"strconv"
	"strings"

	giteaadapter "github.com/orchard9/rdev/internal/adapter/gitea"
	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/logging"
	"github.com/orchard9/rdev/internal/port"
)

// AddComponentBatch adds multiple components in a single atomic operation.
// All components are validated upfront, then committed in a single git commit.
// Infrastructure components (postgres, redis) are provisioned sequentially before code components.
func (s *ComponentService) AddComponentBatch(ctx context.Context, projectID string, reqs []port.AddComponentRequest) ([]*domain.Component, error) {
	if len(reqs) == 0 {
		return nil, fmt.Errorf("at least one component is required")
	}

	log := logging.FromContext(ctx).WithService("component")

	// 1. Validate all components upfront
	var infraReqs []port.AddComponentRequest
	var codeReqs []port.AddComponentRequest

	for _, req := range reqs {
		// Validate component type
		if !domain.IsValidComponentType(req.Type) {
			return nil, fmt.Errorf("%w: %s", domain.ErrInvalidComponentType, req.Type)
		}
		componentType := domain.ComponentType(req.Type)

		// Validate component name
		if err := domain.ValidateComponentName(req.Name); err != nil {
			return nil, fmt.Errorf("%w: %s", err, req.Name)
		}

		// Separate infrastructure from code components
		if componentType.IsInfraComponent() {
			infraReqs = append(infraReqs, req)
		} else {
			codeReqs = append(codeReqs, req)
		}
	}

	// Check for duplicate names in the batch
	seen := make(map[string]bool)
	for _, req := range reqs {
		key := req.Type + ":" + req.Name
		if seen[key] {
			return nil, fmt.Errorf("%w: duplicate component %s/%s in batch", domain.ErrDuplicateComponent, req.Type, req.Name)
		}
		seen[key] = true
	}

	results := make([]*domain.Component, 0, len(reqs))

	// 2. Provision infrastructure components first (these don't need git commits)
	for _, req := range infraReqs {
		componentType := domain.ComponentType(req.Type)
		component, err := s.addInfraComponent(ctx, projectID, componentType, req.Name)
		if err != nil {
			return results, fmt.Errorf("failed to provision %s component %s: %w", req.Type, req.Name, err)
		}
		results = append(results, component)
	}

	// 3. If no code components, we're done
	if len(codeReqs) == 0 {
		return results, nil
	}

	// 4. Get project info from database (needed for code components)
	var gitRepoOwner, gitRepoName string
	var projectDomain string
	err := s.db.QueryRowContext(ctx, `
		SELECT COALESCE(git_repo_owner, $2), COALESCE(git_repo_name, $1), COALESCE(domain, '')
		FROM projects WHERE id = $1
	`, projectID, s.defaultGitOwner).Scan(&gitRepoOwner, &gitRepoName, &projectDomain)
	if err == sql.ErrNoRows {
		return results, fmt.Errorf("%w: %s", domain.ErrProjectNotFound, projectID)
	}
	if err != nil {
		return results, fmt.Errorf("failed to get project: %w", err)
	}

	goModule := fmt.Sprintf("git.threesix.ai/%s/%s", gitRepoOwner, gitRepoName)

	// 5. Prepare all file operations for code components
	var allFileOps []giteaadapter.ChangeFileOperation
	var codeComponents []*domain.Component

	// Track files we've already fetched/modified to avoid duplicate fetches
	type fileState struct {
		content []byte
		sha     string
	}
	fileCache := make(map[string]*fileState)

	// Helper to get file content (cached)
	getFile := func(path string) ([]byte, string, error) {
		if cached, ok := fileCache[path]; ok {
			return cached.content, cached.sha, nil
		}
		content, sha, err := s.bulkClient.GetFileContent(ctx, gitRepoOwner, gitRepoName, path)
		if err != nil {
			return nil, "", err
		}
		fileCache[path] = &fileState{content: content, sha: sha}
		return content, sha, nil
	}

	// 6. Process each code component
	for _, req := range codeReqs {
		componentType := domain.ComponentType(req.Type)
		destDir := componentType.DestDir()
		componentPath := filepath.Join(destDir, req.Name)

		// Check for duplicate component by checking for key files
		checkFile := componentPath + "/go.mod"
		if componentType == domain.ComponentTypeAppAstro || componentType == domain.ComponentTypeAppReact {
			checkFile = componentPath + "/package.json"
		}
		existingContent, _, err := s.bulkClient.GetFileContent(ctx, gitRepoOwner, gitRepoName, checkFile)
		if err != nil {
			return results, fmt.Errorf("failed to check for existing component %s: %w", req.Name, err)
		}
		if existingContent != nil {
			return results, fmt.Errorf("%w: %s", domain.ErrDuplicateComponent, componentPath)
		}

		// Assign port if needed
		port := req.Port
		if port == 0 && componentType.NeedsPort() {
			port, err = s.assignPort(ctx, projectID, componentType)
			if err != nil {
				return results, fmt.Errorf("failed to assign port for %s: %w", req.Name, err)
			}
		}

		// Prepare template variables
		vars := map[string]string{
			"PROJECT_NAME":   projectID,
			"GO_MODULE":      goModule,
			"COMPONENT_NAME": req.Name,
			"PORT":           strconv.Itoa(port),
			"DOMAIN":         projectDomain,
		}

		// Get component template files
		componentFiles, err := s.templateProvider.GetComponentFiles(ctx, req.Type, componentPath, vars)
		if err != nil {
			return results, fmt.Errorf("failed to get component template files for %s: %w", req.Name, err)
		}

		// Add component files to operations
		for _, cf := range componentFiles {
			if strings.HasSuffix(cf.Path, ".woodpecker.step.yml") {
				continue
			}
			encodedContent := base64.StdEncoding.EncodeToString([]byte(cf.Content))
			allFileOps = append(allFileOps, giteaadapter.ChangeFileOperation{
				Operation: "create",
				Path:      cf.Path,
				Content:   encodedContent,
			})
		}

		// Track component for later
		codeComponents = append(codeComponents, &domain.Component{
			Type:         componentType,
			Name:         req.Name,
			Path:         componentPath,
			Port:         port,
			Template:     req.Type,
			Dependencies: []string{},
		})
	}

	// 7. Prepare monorepo file updates (Procfile, go.work, .woodpecker.yml, CLAUDE.md)
	// These need to be accumulated across all components

	// Update Procfile
	procfileContent, procfileSHA, err := getFile("Procfile")
	if err != nil {
		return results, fmt.Errorf("failed to get Procfile: %w", err)
	}
	if procfileContent != nil {
		updatedProcfile := string(procfileContent)
		for i, comp := range codeComponents {
			updatedProcfile = s.updateProcfile(updatedProcfile, comp.Type, comp.Name, comp.Path, comp.Port)
			// Update cache for next iteration
			fileCache["Procfile"] = &fileState{content: []byte(updatedProcfile), sha: procfileSHA}
			_ = i // silence unused
		}
		allFileOps = append(allFileOps, giteaadapter.ChangeFileOperation{
			Operation: "update",
			Path:      "Procfile",
			Content:   base64.StdEncoding.EncodeToString([]byte(updatedProcfile)),
			SHA:       procfileSHA,
		})
	}

	// Update go.work (only for Go components)
	goWorkContent, goWorkSHA, err := getFile("go.work")
	if err != nil {
		return results, fmt.Errorf("failed to get go.work: %w", err)
	}
	if goWorkContent != nil {
		updatedGoWork := string(goWorkContent)
		for _, comp := range codeComponents {
			if comp.Type.IsGoComponent() {
				updatedGoWork = s.updateGoWork(updatedGoWork, comp.Path)
			}
		}
		allFileOps = append(allFileOps, giteaadapter.ChangeFileOperation{
			Operation: "update",
			Path:      "go.work",
			Content:   base64.StdEncoding.EncodeToString([]byte(updatedGoWork)),
			SHA:       goWorkSHA,
		})
	}

	// Update .woodpecker.yml
	woodpeckerContent, woodpeckerSHA, err := getFile(".woodpecker.yml")
	if err != nil {
		return results, fmt.Errorf("failed to get .woodpecker.yml: %w", err)
	}
	if woodpeckerContent != nil {
		updatedWoodpecker := string(woodpeckerContent)
		for i, req := range codeReqs {
			comp := codeComponents[i]
			vars := map[string]string{
				"PROJECT_NAME":   projectID,
				"GO_MODULE":      goModule,
				"COMPONENT_NAME": comp.Name,
				"PORT":           strconv.Itoa(comp.Port),
				"DOMAIN":         projectDomain,
			}
			stepYaml, err := s.templateProvider.GetComponentWoodpeckerStep(ctx, req.Type, vars)
			if err != nil {
				log.Warn("failed to get woodpecker step template", logging.FieldError, err, "component", comp.Name)
				continue
			}
			updatedWoodpecker = s.updateWoodpeckerYml(updatedWoodpecker, stepYaml)
		}
		allFileOps = append(allFileOps, giteaadapter.ChangeFileOperation{
			Operation: "update",
			Path:      ".woodpecker.yml",
			Content:   base64.StdEncoding.EncodeToString([]byte(updatedWoodpecker)),
			SHA:       woodpeckerSHA,
		})
	}

	// Update CLAUDE.md
	claudeMdContent, claudeMdSHA, err := getFile("CLAUDE.md")
	if err != nil {
		return results, fmt.Errorf("failed to get CLAUDE.md: %w", err)
	}
	if claudeMdContent != nil {
		updatedClaudeMd := string(claudeMdContent)
		for _, comp := range codeComponents {
			updatedClaudeMd = s.updateClaudeMd(updatedClaudeMd, comp.Type, comp.Name, comp.Path)
		}
		allFileOps = append(allFileOps, giteaadapter.ChangeFileOperation{
			Operation: "update",
			Path:      "CLAUDE.md",
			Content:   base64.StdEncoding.EncodeToString([]byte(updatedClaudeMd)),
			SHA:       claudeMdSHA,
		})
	}

	// 8. Commit all files in a single atomic commit
	componentNames := make([]string, len(codeReqs))
	for i, req := range codeReqs {
		componentNames[i] = req.Type + "/" + req.Name
	}
	opts := giteaadapter.ChangeFilesOptions{
		Files:   allFileOps,
		Message: fmt.Sprintf("Add components: %s", strings.Join(componentNames, ", ")),
	}

	_, err = s.bulkClient.ChangeFiles(ctx, gitRepoOwner, gitRepoName, opts)
	if err != nil {
		return results, fmt.Errorf("failed to commit component files: %w", err)
	}

	log.Info("batch components added successfully",
		logging.FieldProjectID, projectID,
		"count", len(codeComponents),
		"components", componentNames,
	)

	// 9. Create initial K8s deployments for components that need one
	for _, comp := range codeComponents {
		s.createInitialComponentDeployment(ctx, projectID, projectDomain, comp)
	}

	// 10. Combine infrastructure and code component results
	results = append(results, codeComponents...)

	return results, nil
}
