// Package templates provides embedded project templates for seeding new repos.
//
// Templates are embedded at compile time from the templates/ subdirectory.
// Each template contains starter files with {{VAR}} placeholders that get
// interpolated when seeding a repository.
//
// Single-Commit Seeding:
// Template files are created in a single commit using Gitea's bulk file API
// (POST /repos/{owner}/{repo}/contents). This prevents multiple CI pipeline
// triggers that would occur with per-file commits.
package templates

import (
	"context"
	"embed"
	"encoding/base64"
	"fmt"
	"io/fs"
	"log/slog"
	"path/filepath"
	"regexp"
	"strings"

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

//go:embed all:templates
var templatesFS embed.FS

// availableTemplates lists all supported project templates.
var availableTemplates = []port.TemplateInfo{
	{Name: "default", Description: "Basic project with Dockerfile", Stack: "generic"},
	{Name: "astro-landing", Description: "Astro landing page with Tailwind", Stack: "astro"},
	{Name: "go-api", Description: "Go REST API with chi router", Stack: "go"},
}

// skeletonTemplate is the monorepo skeleton template used for composable projects.
var skeletonTemplate = port.TemplateInfo{
	Name:        "skeleton",
	Description: "Composable monorepo skeleton with services, workers, apps, and CLI directories",
	Stack:       "monorepo",
}

// availableComponentTemplates lists all supported component templates.
var availableComponentTemplates = []port.ComponentTemplateInfo{
	{
		Type:        "service",
		Description: "Go API service using pkg/ shared packages",
		Stack:       "go",
		DefaultPort: 8080,
		DestDir:     "services",
	},
	{
		Type:        "worker",
		Description: "Go background worker for async job processing",
		Stack:       "go",
		DefaultPort: 0, // Workers don't expose ports
		DestDir:     "workers",
	},
	{
		Type:        "app-astro",
		Description: "Astro landing page with Tailwind CSS",
		Stack:       "astro",
		DefaultPort: 4321,
		DestDir:     "apps",
	},
	{
		Type:        "app-react",
		Description: "React SPA with Vite, TypeScript, and Tailwind",
		Stack:       "react",
		DefaultPort: 5173,
		DestDir:     "apps",
	},
	{
		Type:        "app-nextjs",
		Description: "Next.js 14 dashboard with App Router and design system",
		Stack:       "nextjs",
		DefaultPort: 3000,
		DestDir:     "apps",
	},
	{
		Type:        "cli",
		Description: "Go CLI tool using Cobra",
		Stack:       "go",
		DefaultPort: 0, // CLIs don't expose ports
		DestDir:     "cli",
	},
}

// templateNameRegex validates template names (alphanumeric, dash only).
var templateNameRegex = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)

// Provider implements port.TemplateProvider using embedded templates
// and the Gitea API to seed repositories.
type Provider struct {
	bulkClient *giteaadapter.BulkFileClient
	logger     *slog.Logger
}

// Ensure Provider implements TemplateProvider.
var _ port.TemplateProvider = (*Provider)(nil)

// NewProvider creates a new template provider.
// giteaURL is the Gitea server URL (e.g., https://git.threesix.ai)
// giteaToken is an API access token with repo permissions
// logger is optional; if nil, slog.Default() is used.
func NewProvider(giteaURL, giteaToken string, logger *slog.Logger) *Provider {
	if logger == nil {
		logger = slog.Default()
	}
	return &Provider{
		bulkClient: giteaadapter.NewBulkFileClient(giteaURL, giteaToken),
		logger:     logger,
	}
}

// SeedRepo populates a repository with template files in a single commit.
// All template files are collected and committed atomically using Gitea's
// bulk file API, preventing multiple CI pipeline triggers.
func (p *Provider) SeedRepo(ctx context.Context, owner, repo, templateName string, vars map[string]string) error {
	// Check for context cancellation
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	// Validate template name (prevent path traversal, enforce naming convention)
	if !isValidTemplateName(templateName) {
		return fmt.Errorf("invalid template name: %s (must be lowercase alphanumeric with dashes)", templateName)
	}

	// Validate template exists
	templateDir := "templates/" + templateName
	if _, err := templatesFS.ReadDir(templateDir); err != nil {
		return fmt.Errorf("template not found: %s", templateName)
	}

	p.logger.Info("seeding repo from template",
		"owner", owner,
		"repo", repo,
		"template", templateName,
	)

	// Collect all template files
	var fileOps []giteaadapter.ChangeFileOperation
	err := fs.WalkDir(templatesFS, templateDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}

		// Read file content
		content, err := templatesFS.ReadFile(path)
		if err != nil {
			return fmt.Errorf("failed to read template file %s: %w", path, err)
		}

		// Interpolate variables
		interpolated := interpolateVars(string(content), vars)

		// Calculate relative path from template root
		relPath, err := filepath.Rel(templateDir, path)
		if err != nil {
			return fmt.Errorf("failed to get relative path: %w", err)
		}

		// Strip .tmpl extension (allows embedding go.mod as go.mod.tmpl)
		relPath = strings.TrimSuffix(relPath, ".tmpl")

		// Gitea expects base64-encoded content
		encodedContent := base64.StdEncoding.EncodeToString([]byte(interpolated))

		fileOps = append(fileOps, giteaadapter.ChangeFileOperation{
			Operation: "create",
			Path:      relPath,
			Content:   encodedContent,
		})

		return nil
	})

	if err != nil {
		return fmt.Errorf("failed to collect template files: %w", err)
	}

	if len(fileOps) == 0 {
		return fmt.Errorf("template %s contains no files", templateName)
	}

	// Create all files in a single commit
	opts := giteaadapter.ChangeFilesOptions{
		Files:         fileOps,
		Message:       fmt.Sprintf("Initialize project from %s template", templateName),
		NewBranchName: "main", // Create the main branch (repo is empty)
	}

	_, err = p.bulkClient.ChangeFiles(ctx, owner, repo, opts)
	if err != nil {
		return fmt.Errorf("failed to seed repo from template %s: %w", templateName, err)
	}

	p.logger.Info("repo seeded successfully",
		"owner", owner,
		"repo", repo,
		"template", templateName,
		"files_created", len(fileOps),
		"commit_message", opts.Message,
	)

	return nil
}

// isValidTemplateName validates that a template name is safe.
func isValidTemplateName(name string) bool {
	if name == "" || len(name) > 64 {
		return false
	}
	return templateNameRegex.MatchString(name)
}

// ListTemplates returns available templates.
func (p *Provider) ListTemplates(ctx context.Context) ([]port.TemplateInfo, error) {
	// Check for context cancellation
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	result := make([]port.TemplateInfo, len(availableTemplates))
	for i, t := range availableTemplates {
		result[i] = t
		// Populate files list
		files, err := listTemplateFiles(t.Name)
		if err == nil {
			result[i].Files = files
		}
	}
	return result, nil
}

// GetTemplate returns info about a specific template.
func (p *Provider) GetTemplate(ctx context.Context, name string) (*port.TemplateInfo, error) {
	// Check for context cancellation
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	for _, t := range availableTemplates {
		if t.Name == name {
			result := t
			files, err := listTemplateFiles(name)
			if err == nil {
				result.Files = files
			}
			return &result, nil
		}
	}
	return nil, fmt.Errorf("%w: %s", domain.ErrTemplateNotFound, name)
}

// interpolateVars replaces {{VAR_NAME}} placeholders with values.
func interpolateVars(content string, vars map[string]string) string {
	result := content
	for key, value := range vars {
		placeholder := "{{" + key + "}}"
		result = strings.ReplaceAll(result, placeholder, value)
	}
	return result
}

// listTemplateFiles returns the list of files in a template.
func listTemplateFiles(templateName string) ([]string, error) {
	templateDir := "templates/" + templateName
	var files []string

	err := fs.WalkDir(templatesFS, templateDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		relPath, err := filepath.Rel(templateDir, path)
		if err != nil {
			return err
		}
		// Strip .tmpl extension for display
		relPath = strings.TrimSuffix(relPath, ".tmpl")
		files = append(files, relPath)
		return nil
	})

	return files, err
}

// SeedSkeleton populates a repository with the monorepo skeleton template.
// This creates the base monorepo structure without any components.
func (p *Provider) SeedSkeleton(ctx context.Context, owner, repo string, vars map[string]string) error {
	return p.SeedRepo(ctx, owner, repo, "skeleton", vars)
}

// GetSkeleton returns info about the monorepo skeleton template.
func (p *Provider) GetSkeleton(ctx context.Context) (*port.TemplateInfo, error) {
	// Check for context cancellation
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	result := skeletonTemplate
	files, err := listTemplateFiles("skeleton")
	if err == nil {
		result.Files = files
	}
	return &result, nil
}

// GetComponentTemplate returns info about a specific component template.
func (p *Provider) GetComponentTemplate(ctx context.Context, componentType string) (*port.ComponentTemplateInfo, error) {
	// Check for context cancellation
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	for _, t := range availableComponentTemplates {
		if t.Type == componentType {
			result := t
			files, err := listComponentTemplateFiles(componentType)
			if err == nil {
				result.Files = files
			}
			return &result, nil
		}
	}
	return nil, fmt.Errorf("%w: component type %s", domain.ErrTemplateNotFound, componentType)
}

// ListComponentTemplates returns available component templates.
// If componentType is empty, returns all templates; otherwise filters by type.
func (p *Provider) ListComponentTemplates(ctx context.Context, componentType string) ([]port.ComponentTemplateInfo, error) {
	// Check for context cancellation
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	var result []port.ComponentTemplateInfo
	for _, t := range availableComponentTemplates {
		if componentType != "" && t.Type != componentType {
			continue
		}
		info := t
		files, err := listComponentTemplateFiles(t.Type)
		if err == nil {
			info.Files = files
		}
		result = append(result, info)
	}
	return result, nil
}

// GetComponentFiles returns the files for a component template with variables interpolated.
func (p *Provider) GetComponentFiles(ctx context.Context, componentType string, destPath string, vars map[string]string) ([]port.ComponentFile, error) {
	// Check for context cancellation
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	// Validate component type exists
	found := false
	for _, t := range availableComponentTemplates {
		if t.Type == componentType {
			found = true
			break
		}
	}
	if !found {
		return nil, fmt.Errorf("%w: component type %s", domain.ErrTemplateNotFound, componentType)
	}

	templateDir := "templates/components/" + componentType
	var files []port.ComponentFile

	err := fs.WalkDir(templatesFS, templateDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}

		// Read file content
		content, err := templatesFS.ReadFile(path)
		if err != nil {
			return fmt.Errorf("failed to read component template file %s: %w", path, err)
		}

		// Interpolate variables
		interpolated := interpolateVars(string(content), vars)

		// Calculate relative path from component template root
		relPath, err := filepath.Rel(templateDir, path)
		if err != nil {
			return fmt.Errorf("failed to get relative path: %w", err)
		}

		// Strip .tmpl extension
		relPath = strings.TrimSuffix(relPath, ".tmpl")

		// Prepend destination path
		fullPath := filepath.Join(destPath, relPath)

		files = append(files, port.ComponentFile{
			Path:    fullPath,
			Content: interpolated,
		})

		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to collect component template files: %w", err)
	}

	if len(files) == 0 {
		return nil, fmt.Errorf("component template %s contains no files", componentType)
	}

	p.logger.Debug("prepared component files",
		"component_type", componentType,
		"dest_path", destPath,
		"file_count", len(files),
	)

	return files, nil
}

// listComponentTemplateFiles returns the list of files in a component template.
func listComponentTemplateFiles(componentType string) ([]string, error) {
	templateDir := "templates/components/" + componentType
	var files []string

	err := fs.WalkDir(templatesFS, templateDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		relPath, err := filepath.Rel(templateDir, path)
		if err != nil {
			return err
		}
		// Strip .tmpl extension for display
		relPath = strings.TrimSuffix(relPath, ".tmpl")
		files = append(files, relPath)
		return nil
	})

	return files, err
}

// GetComponentWoodpeckerStep returns the .woodpecker.step.yml content for a component.
// This is the CI step that should be inserted into the main .woodpecker.yml file.
func (p *Provider) GetComponentWoodpeckerStep(ctx context.Context, componentType string, vars map[string]string) (string, error) {
	// Check for context cancellation
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	default:
	}

	stepPath := "templates/components/" + componentType + "/.woodpecker.step.yml.tmpl"
	content, err := templatesFS.ReadFile(stepPath)
	if err != nil {
		return "", fmt.Errorf("failed to read woodpecker step template: %w", err)
	}

	return interpolateVars(string(content), vars), nil
}
