package overlay

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"sub2api-cn-relay-manager/internal/pack"
)

const metadataFileName = ".sub2api-cn-relay-manager-overlay.json"

type ApplyRequest struct {
	PackDir   string
	SourceDir string
	OutputDir string
	Overlays  []pack.HostOverlay
}

type ApplyResult struct {
	OutputDir        string
	AppliedOverlays  []pack.HostOverlay
	MetadataFilePath string
}

func Apply(ctx context.Context, req ApplyRequest) (_ ApplyResult, err error) {
	packDir := strings.TrimSpace(req.PackDir)
	sourceDir := strings.TrimSpace(req.SourceDir)
	if packDir == "" {
		return ApplyResult{}, fmt.Errorf("pack dir is required")
	}
	if sourceDir == "" {
		return ApplyResult{}, fmt.Errorf("source dir is required")
	}
	if len(req.Overlays) == 0 {
		return ApplyResult{}, fmt.Errorf("at least one host overlay is required")
	}

	packAbs, err := filepath.Abs(packDir)
	if err != nil {
		return ApplyResult{}, fmt.Errorf("resolve pack dir: %w", err)
	}
	sourceAbs, err := filepath.Abs(sourceDir)
	if err != nil {
		return ApplyResult{}, fmt.Errorf("resolve source dir: %w", err)
	}
	sourceInfo, err := os.Stat(sourceAbs)
	if err != nil {
		return ApplyResult{}, fmt.Errorf("stat source dir: %w", err)
	}
	if !sourceInfo.IsDir() {
		return ApplyResult{}, fmt.Errorf("source dir %q must be a directory", sourceAbs)
	}

	outputDir := strings.TrimSpace(req.OutputDir)
	if outputDir == "" {
		outputDir = defaultOutputDir(sourceAbs, req.Overlays)
	}
	outputAbs, err := filepath.Abs(outputDir)
	if err != nil {
		return ApplyResult{}, fmt.Errorf("resolve output dir: %w", err)
	}
	if outputAbs == sourceAbs {
		return ApplyResult{}, fmt.Errorf("output dir must differ from source dir")
	}
	if isPathWithin(outputAbs, sourceAbs) {
		return ApplyResult{}, fmt.Errorf("output dir %q must not be nested inside source dir %q", outputAbs, sourceAbs)
	}
	if _, err := os.Stat(outputAbs); err == nil {
		return ApplyResult{}, fmt.Errorf("output dir %q already exists", outputAbs)
	} else if !os.IsNotExist(err) {
		return ApplyResult{}, fmt.Errorf("stat output dir: %w", err)
	}

	if err := copyTree(sourceAbs, outputAbs); err != nil {
		return ApplyResult{}, fmt.Errorf("copy source dir: %w", err)
	}
	cleanupOutput := true
	defer func() {
		if cleanupOutput {
			_ = os.RemoveAll(outputAbs)
		}
	}()

	for _, hostOverlay := range req.Overlays {
		patchPath := strings.TrimSpace(hostOverlay.PatchPath)
		if patchPath == "" {
			return ApplyResult{}, fmt.Errorf("overlay %q does not define patch_path", hostOverlay.OverlayID)
		}
		patchAbs := filepath.Join(packAbs, patchPath)
		if err := applyPatchFile(ctx, outputAbs, patchAbs); err != nil {
			return ApplyResult{}, fmt.Errorf("apply overlay %q: %w", hostOverlay.OverlayID, err)
		}
	}

	metadataPath := filepath.Join(outputAbs, metadataFileName)
	if err := writeMetadata(metadataPath, sourceAbs, req.Overlays); err != nil {
		return ApplyResult{}, fmt.Errorf("write overlay metadata: %w", err)
	}

	cleanupOutput = false
	return ApplyResult{
		OutputDir:        outputAbs,
		AppliedOverlays:  append([]pack.HostOverlay(nil), req.Overlays...),
		MetadataFilePath: metadataPath,
	}, nil
}

func FilterOverlays(overlays []pack.HostOverlay, overlayID string) ([]pack.HostOverlay, error) {
	trimmedOverlayID := strings.TrimSpace(overlayID)
	if trimmedOverlayID == "" {
		return append([]pack.HostOverlay(nil), overlays...), nil
	}
	filtered := make([]pack.HostOverlay, 0, len(overlays))
	for _, hostOverlay := range overlays {
		if strings.TrimSpace(hostOverlay.OverlayID) == trimmedOverlayID {
			filtered = append(filtered, hostOverlay)
		}
	}
	if len(filtered) == 0 {
		return nil, fmt.Errorf("overlay %q did not match any resolved host overlays", trimmedOverlayID)
	}
	return filtered, nil
}

func applyPatchFile(ctx context.Context, outputDir string, patchPath string) error {
	if _, err := os.Stat(patchPath); err != nil {
		return fmt.Errorf("stat patch file %q: %w", patchPath, err)
	}
	cmd := exec.CommandContext(ctx, "patch", "-p1", "-i", patchPath, "-d", outputDir)
	output, err := cmd.CombinedOutput()
	if err != nil {
		message := strings.TrimSpace(string(output))
		if message == "" {
			message = err.Error()
		}
		return fmt.Errorf("%s", message)
	}
	return nil
}

func writeMetadata(path string, sourceDir string, overlays []pack.HostOverlay) error {
	body, err := json.MarshalIndent(map[string]any{
		"source_dir":       sourceDir,
		"applied_overlays": overlays,
	}, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, append(body, '\n'), 0o644)
}

func defaultOutputDir(sourceDir string, overlays []pack.HostOverlay) string {
	baseName := filepath.Base(sourceDir)
	overlaySuffix := "overlay"
	if len(overlays) > 0 {
		overlaySuffix = sanitizePathToken(overlays[0].OverlayID)
		if overlaySuffix == "" {
			overlaySuffix = "overlay"
		}
	}
	return filepath.Join(filepath.Dir(sourceDir), baseName+"-patched-"+overlaySuffix)
}

func sanitizePathToken(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	var b strings.Builder
	lastDash := false
	for _, r := range value {
		switch {
		case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
			b.WriteRune(r)
			lastDash = false
		case !lastDash:
			b.WriteByte('-')
			lastDash = true
		}
	}
	return strings.Trim(b.String(), "-")
}

func isPathWithin(target string, root string) bool {
	rel, err := filepath.Rel(root, target)
	if err != nil {
		return false
	}
	return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
}

func copyTree(sourceDir string, outputDir string) error {
	if err := os.MkdirAll(outputDir, 0o755); err != nil {
		return err
	}
	return filepath.WalkDir(sourceDir, func(path string, d fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		rel, err := filepath.Rel(sourceDir, path)
		if err != nil {
			return err
		}
		if rel == "." {
			return nil
		}
		if rel == ".git" || strings.HasPrefix(rel, ".git"+string(filepath.Separator)) {
			if d.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}
		targetPath := filepath.Join(outputDir, rel)
		info, err := d.Info()
		if err != nil {
			return err
		}
		switch {
		case d.IsDir():
			return os.MkdirAll(targetPath, info.Mode().Perm())
		case info.Mode()&os.ModeSymlink != 0:
			linkTarget, err := os.Readlink(path)
			if err != nil {
				return err
			}
			return os.Symlink(linkTarget, targetPath)
		default:
			return copyFile(path, targetPath, info.Mode().Perm())
		}
	})
}

func copyFile(sourcePath string, targetPath string, perm fs.FileMode) error {
	if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
		return err
	}
	sourceFile, err := os.Open(sourcePath)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	targetFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm)
	if err != nil {
		return err
	}
	defer targetFile.Close()

	if _, err := io.Copy(targetFile, sourceFile); err != nil {
		return err
	}
	return nil
}
