// Package service provides business logic services.
package service

import (
	"context"
	"database/sql"
	"encoding/base64"
	"fmt"
	"path/filepath"
	"regexp"
	"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"
)

// procfilePatterns contains pre-compiled regex patterns for parsing Procfile entries
// by component type. Compiled once at package init for performance.
var procfilePatterns = make(map[domain.ComponentType]*regexp.Regexp)

func init() {
	// Pre-compile regex patterns for each component type's Procfile entries
	for _, ct := range domain.ValidComponentTypes {
		destDir := ct.DestDir()
		if destDir != "" {
			// Pattern matches: "component-name: cd services/component-name && ..."
			procfilePatterns[ct] = regexp.MustCompile(`^([a-z][a-z0-9-]*): cd (` + destDir + `/[a-z0-9-]+)`)
		}
	}
}

// ComponentService manages components within monorepo projects.
type ComponentService struct {
	db               *sql.DB
	templateProvider port.TemplateProvider
	bulkClient       *giteaadapter.BulkFileClient
	deployer         port.Deployer
	defaultGitOwner  string
	registryURL      string

	// Infrastructure provisioners (optional - needed for postgres/redis/gcs components)
	dbProvisioner      port.DatabaseProvisioner
	cacheProvisioner   port.CacheProvisioner
	storageProvisioner port.StorageProvisioner
	credentialStore    port.CredentialStore
}

// ComponentServiceConfig configures the component service.
type ComponentServiceConfig struct {
	DefaultGitOwner string // e.g., "threesix"
	RegistryURL     string // e.g., "registry.threesix.ai"
}

// NewComponentService creates a new component service.
func NewComponentService(
	db *sql.DB,
	templateProvider port.TemplateProvider,
	bulkClient *giteaadapter.BulkFileClient,
	deployer port.Deployer,
	cfg ComponentServiceConfig,
) *ComponentService {
	return &ComponentService{
		db:               db,
		templateProvider: templateProvider,
		bulkClient:       bulkClient,
		deployer:         deployer,
		defaultGitOwner:  cfg.DefaultGitOwner,
		registryURL:      cfg.RegistryURL,
	}
}

// Ensure ComponentService implements the interface.
var _ port.ComponentService = (*ComponentService)(nil)

// WithDatabaseProvisioner adds a database provisioner for postgres component support.
func (s *ComponentService) WithDatabaseProvisioner(p port.DatabaseProvisioner) *ComponentService {
	s.dbProvisioner = p
	return s
}

// WithCacheProvisioner adds a cache provisioner for redis component support.
func (s *ComponentService) WithCacheProvisioner(p port.CacheProvisioner) *ComponentService {
	s.cacheProvisioner = p
	return s
}

// WithStorageProvisioner adds a storage provisioner for gcs component support.
func (s *ComponentService) WithStorageProvisioner(p port.StorageProvisioner) *ComponentService {
	s.storageProvisioner = p
	return s
}

// WithCredentialStore adds a credential store for storing provisioned credentials.
func (s *ComponentService) WithCredentialStore(cs port.CredentialStore) *ComponentService {
	s.credentialStore = cs
	return s
}

// AddComponent adds a new component to a project's monorepo.
// For code components (service, worker, app-*, cli), this scaffolds template files.
// For infrastructure components (postgres, redis), this provisions the resource.
func (s *ComponentService) AddComponent(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error) {
	// 1. Validate component type
	if !domain.IsValidComponentType(req.Type) {
		return nil, fmt.Errorf("%w: %s", domain.ErrInvalidComponentType, req.Type)
	}
	componentType := domain.ComponentType(req.Type)

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

	// 3. Route infrastructure components to provisioning
	if componentType.IsInfraComponent() {
		return s.addInfraComponent(ctx, projectID, componentType, req.Name)
	}

	// --- Code component flow (service, worker, app-*, cli) ---

	// 4. Get project info from database
	var gitRepoOwner, gitRepoName, goModule 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 nil, fmt.Errorf("%w: %s", domain.ErrProjectNotFound, projectID)
	}
	if err != nil {
		return nil, fmt.Errorf("failed to get project: %w", err)
	}

	// Build Go module path (use actual git host, not github.com)
	goModule = fmt.Sprintf("git.threesix.ai/%s/%s", gitRepoOwner, gitRepoName)

	// 4. Calculate component path
	destDir := componentType.DestDir()
	componentPath := filepath.Join(destDir, req.Name)

	// 5. 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 nil, fmt.Errorf("failed to check for existing component: %w", err)
	}
	if existingContent != nil {
		return nil, fmt.Errorf("%w: %s", domain.ErrDuplicateComponent, componentPath)
	}

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

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

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

	// 9. Read and update monorepo files
	fileOps, err := s.prepareMonorepoUpdates(ctx, gitRepoOwner, gitRepoName, componentType, req.Name, componentPath, port, vars)
	if err != nil {
		return nil, fmt.Errorf("failed to prepare monorepo updates: %w", err)
	}

	// 10. Add component files to the operations
	for _, cf := range componentFiles {
		// Skip the .woodpecker.step.yml file - it's merged into main .woodpecker.yml
		if strings.HasSuffix(cf.Path, ".woodpecker.step.yml") {
			continue
		}
		encodedContent := base64.StdEncoding.EncodeToString([]byte(cf.Content))
		fileOps = append(fileOps, giteaadapter.ChangeFileOperation{
			Operation: "create",
			Path:      cf.Path,
			Content:   encodedContent,
		})
	}

	// 11. Commit all files in a single atomic commit
	opts := giteaadapter.ChangeFilesOptions{
		Files:   fileOps,
		Message: fmt.Sprintf("Add %s component: %s", req.Type, req.Name),
	}

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

	log := logging.FromContext(ctx).WithService("component")
	log.Info("component added successfully",
		logging.FieldProjectID, projectID,
		"component_type", req.Type,
		"component_name", req.Name,
		"path", componentPath,
		"port", port,
	)

	// 12. Build and return the component
	component := &domain.Component{
		Type:         componentType,
		Name:         req.Name,
		Path:         componentPath,
		Port:         port,
		Template:     req.Type,
		Dependencies: []string{}, // Could be parsed from component.yaml
	}

	// 13. Create initial K8s deployment for components that need one.
	// This ensures kubectl set image will find the deployment when CI runs.
	s.createInitialComponentDeployment(ctx, projectID, projectDomain, component)

	return component, nil
}

// prepareMonorepoUpdates reads existing monorepo files and prepares updates.
func (s *ComponentService) prepareMonorepoUpdates(
	ctx context.Context,
	owner, repo string,
	componentType domain.ComponentType,
	componentName, componentPath string,
	port int,
	vars map[string]string,
) ([]giteaadapter.ChangeFileOperation, error) {
	var fileOps []giteaadapter.ChangeFileOperation

	// 1. Update Procfile
	procfileContent, procfileSHA, err := s.bulkClient.GetFileContent(ctx, owner, repo, "Procfile")
	if err != nil {
		return nil, fmt.Errorf("failed to get Procfile: %w", err)
	}
	if procfileContent != nil {
		updated := s.updateProcfile(string(procfileContent), componentType, componentName, componentPath, port)
		fileOps = append(fileOps, giteaadapter.ChangeFileOperation{
			Operation: "update",
			Path:      "Procfile",
			Content:   base64.StdEncoding.EncodeToString([]byte(updated)),
			SHA:       procfileSHA,
		})
	}

	// 2. Update go.work for Go components
	if componentType.IsGoComponent() {
		goWorkContent, goWorkSHA, err := s.bulkClient.GetFileContent(ctx, owner, repo, "go.work")
		if err != nil {
			return nil, fmt.Errorf("failed to get go.work: %w", err)
		}
		if goWorkContent != nil {
			updated := s.updateGoWork(string(goWorkContent), componentPath)
			fileOps = append(fileOps, giteaadapter.ChangeFileOperation{
				Operation: "update",
				Path:      "go.work",
				Content:   base64.StdEncoding.EncodeToString([]byte(updated)),
				SHA:       goWorkSHA,
			})
		}
	}

	// 3. Update .woodpecker.yml
	woodpeckerContent, woodpeckerSHA, err := s.bulkClient.GetFileContent(ctx, owner, repo, ".woodpecker.yml")
	if err != nil {
		return nil, fmt.Errorf("failed to get .woodpecker.yml: %w", err)
	}
	if woodpeckerContent != nil {
		// Get the CI step template for this component type
		stepYaml, err := s.templateProvider.GetComponentWoodpeckerStep(ctx, string(componentType), vars)
		if err != nil {
			log := logging.FromContext(ctx).WithService("component")
			log.Warn("failed to get woodpecker step template", logging.FieldError, err)
		} else {
			updated := s.updateWoodpeckerYml(string(woodpeckerContent), stepYaml)
			fileOps = append(fileOps, giteaadapter.ChangeFileOperation{
				Operation: "update",
				Path:      ".woodpecker.yml",
				Content:   base64.StdEncoding.EncodeToString([]byte(updated)),
				SHA:       woodpeckerSHA,
			})
		}
	}

	// 4. Update CLAUDE.md
	claudeMdContent, claudeMdSHA, err := s.bulkClient.GetFileContent(ctx, owner, repo, "CLAUDE.md")
	if err != nil {
		return nil, fmt.Errorf("failed to get CLAUDE.md: %w", err)
	}
	if claudeMdContent != nil {
		updated := s.updateClaudeMd(string(claudeMdContent), componentType, componentName, componentPath)
		fileOps = append(fileOps, giteaadapter.ChangeFileOperation{
			Operation: "update",
			Path:      "CLAUDE.md",
			Content:   base64.StdEncoding.EncodeToString([]byte(updated)),
			SHA:       claudeMdSHA,
		})
	}

	return fileOps, nil
}
