package automation

import (
	"archive/tar"
	"compress/gzip"
	"context"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/observability/logging"
	"github.com/tidbcloud/AgentTeam/internal/service/automation/runtime"
)

// AutomationRepositoryArchiveRequest asks an AGS-backed archive provider to
// stream one repository snapshot at a resolved commit.
type AutomationRepositoryArchiveRequest struct {
	RepositoryFullName string
	CommitSHA          string
	Writer             io.Writer
}

// AutomationRepositoryArchiver is the MeshX-side port for AGS archive snapshots.
type AutomationRepositoryArchiver interface {
	ArchiveRepository(ctx context.Context, request AutomationRepositoryArchiveRequest) error
}

// AGSAutomationCheckoutMaterializer materializes current-run AGS repository
// snapshots into server-runner workspaces.
type AGSAutomationCheckoutMaterializer struct {
	Archiver AutomationRepositoryArchiver
}

func (m AGSAutomationCheckoutMaterializer) Materialize(ctx context.Context, req runtime.CheckoutRequest) (runtime.CheckoutResult, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	if m.Archiver == nil {
		return runtime.CheckoutResult{}, fmt.Errorf("automation checkout archive provider is not configured")
	}
	repositoryFullName := strings.TrimSpace(req.RepositoryFullName)
	commitSHA := strings.TrimSpace(req.CommitSHA)
	if repositoryFullName == "" || commitSHA == "" {
		return runtime.CheckoutResult{}, fmt.Errorf("automation checkout requires repository and commit")
	}
	destination, err := prepareAutomationCheckoutDestination(req.Destination, req.Clean)
	if err != nil {
		return runtime.CheckoutResult{}, err
	}

	started := time.Now()
	archiveCtx, cancel := context.WithCancel(ctx)
	defer cancel()
	reader, writer := io.Pipe()
	stopCancelRead := context.AfterFunc(archiveCtx, func() {
		_ = reader.CloseWithError(archiveCtx.Err())
	})
	defer stopCancelRead()
	errCh := make(chan error, 1)
	logging.Go(nil, func() {
		err := m.Archiver.ArchiveRepository(archiveCtx, AutomationRepositoryArchiveRequest{
			RepositoryFullName: repositoryFullName,
			CommitSHA:          commitSHA,
			Writer:             writer,
		})
		if err != nil {
			_ = writer.CloseWithError(err)
		} else {
			_ = writer.Close()
		}
		errCh <- err
	})

	result, extractErr := extractAutomationCheckoutArchive(archiveCtx, reader, destination, req.MaxBytes, req.MaxFiles)
	if extractErr != nil {
		cancel()
		_ = reader.CloseWithError(extractErr)
		<-errCh
		return result, extractErr
	}
	if err := <-errCh; err != nil {
		return result, fmt.Errorf("stream automation checkout archive: %w", err)
	}
	result.Strategy = "ags_archive"
	result.Duration = time.Since(started)
	return result, nil
}

func prepareAutomationCheckoutDestination(destination string, clean bool) (string, error) {
	if strings.TrimSpace(destination) == "" {
		return "", fmt.Errorf("automation checkout destination is required")
	}
	abs, err := filepath.Abs(destination)
	if err != nil {
		return "", fmt.Errorf("resolve automation checkout destination: %w", err)
	}
	if err := rejectAutomationCheckoutSymlinkAncestors(abs); err != nil {
		return "", err
	}
	if clean {
		if err := os.RemoveAll(abs); err != nil {
			return "", fmt.Errorf("clean automation checkout destination: %w", err)
		}
	} else if info, err := os.Lstat(abs); err == nil {
		if info.Mode()&os.ModeSymlink != 0 {
			return "", fmt.Errorf("automation checkout destination must not be a symlink")
		}
		if !info.IsDir() {
			return "", fmt.Errorf("automation checkout destination must be a directory")
		}
	} else if !os.IsNotExist(err) {
		return "", fmt.Errorf("inspect automation checkout destination: %w", err)
	}
	if err := os.MkdirAll(abs, 0o755); err != nil {
		return "", fmt.Errorf("create automation checkout destination: %w", err)
	}
	return abs, nil
}

func rejectAutomationCheckoutSymlinkAncestors(abs string) error {
	dir := filepath.Dir(filepath.Clean(abs))
	for {
		info, err := os.Lstat(dir)
		if err == nil {
			if info.Mode()&os.ModeSymlink != 0 {
				return fmt.Errorf("automation checkout destination parent %q is a symlink", dir)
			}
			if !info.IsDir() {
				return fmt.Errorf("automation checkout destination parent %q is not a directory", dir)
			}
		} else if !os.IsNotExist(err) {
			return fmt.Errorf("inspect automation checkout destination parent %q: %w", dir, err)
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return nil
		}
		dir = parent
	}
}

func extractAutomationCheckoutArchive(ctx context.Context, reader io.Reader, destination string, maxBytes int64, maxFiles int) (runtime.CheckoutResult, error) {
	gzipReader, err := gzip.NewReader(contextReader{ctx: ctx, reader: reader})
	if err != nil {
		return runtime.CheckoutResult{}, fmt.Errorf("open automation checkout archive: %w", err)
	}
	defer gzipReader.Close()

	tarReader := tar.NewReader(gzipReader)
	var result runtime.CheckoutResult
	verifiedParents := map[string]struct{}{filepath.Clean(destination): {}}
	for {
		if err := ctx.Err(); err != nil {
			return result, err
		}
		header, err := tarReader.Next()
		if err == io.EOF {
			return result, nil
		}
		if err != nil {
			return result, fmt.Errorf("read automation checkout archive: %w", err)
		}
		if isAutomationCheckoutArchiveMetadata(header.Typeflag) {
			continue
		}
		relPath, err := automationCheckoutArchivePath(header.Name)
		if err != nil {
			return result, err
		}
		target := filepath.Join(destination, filepath.FromSlash(relPath))
		if err := ensureAutomationCheckoutTarget(destination, target); err != nil {
			return result, err
		}
		switch header.Typeflag {
		case tar.TypeDir:
			if err := ensureNoCheckoutSymlinkAncestor(destination, target, verifiedParents); err != nil {
				return result, err
			}
			mode := checkoutDirMode(header)
			if err := os.MkdirAll(target, mode); err != nil {
				return result, fmt.Errorf("create automation checkout directory %q: %w", relPath, err)
			}
			if err := os.Chmod(target, mode); err != nil {
				return result, fmt.Errorf("set automation checkout directory mode %q: %w", relPath, err)
			}
			verifiedParents[filepath.Clean(target)] = struct{}{}
		case tar.TypeReg, tar.TypeRegA:
			if err := incrementAutomationCheckoutFiles(&result, maxFiles); err != nil {
				return result, err
			}
			if header.Size < 0 {
				return result, fmt.Errorf("automation checkout archive file %q has invalid size", relPath)
			}
			if maxBytes > 0 && result.Bytes+header.Size > maxBytes {
				return result, fmt.Errorf("automation checkout archive exceeds %d bytes", maxBytes)
			}
			if err := writeAutomationCheckoutFile(ctx, destination, target, relPath, header, tarReader, verifiedParents); err != nil {
				return result, err
			}
			result.Bytes += header.Size
		case tar.TypeSymlink:
			if err := incrementAutomationCheckoutFiles(&result, maxFiles); err != nil {
				return result, err
			}
			if err := createAutomationCheckoutSymlink(destination, target, relPath, header.Linkname, verifiedParents); err != nil {
				return result, err
			}
		default:
			return result, fmt.Errorf("automation checkout archive entry %q has unsupported type %d", relPath, header.Typeflag)
		}
	}
}

func isAutomationCheckoutArchiveMetadata(typeflag byte) bool {
	switch typeflag {
	case tar.TypeXHeader, tar.TypeXGlobalHeader, tar.TypeGNULongName, tar.TypeGNULongLink:
		return true
	default:
		return false
	}
}

func automationCheckoutArchivePath(raw string) (string, error) {
	name := filepath.ToSlash(raw)
	if name == "" || strings.HasPrefix(name, "/") || path.IsAbs(name) {
		return "", fmt.Errorf("automation checkout archive contains unsafe path %q", raw)
	}
	name = strings.TrimRight(name, "/")
	if name == "" {
		return "", fmt.Errorf("automation checkout archive contains unsafe path %q", raw)
	}
	segments := strings.Split(name, "/")
	for _, segment := range segments {
		if segment == "" || segment == "." || segment == ".." || strings.EqualFold(segment, ".git") {
			return "", fmt.Errorf("automation checkout archive contains unsafe path %q", raw)
		}
	}
	return name, nil
}

func ensureAutomationCheckoutTarget(root string, target string) error {
	cleanRoot := filepath.Clean(root)
	cleanTarget := filepath.Clean(target)
	if cleanTarget != cleanRoot && !strings.HasPrefix(cleanTarget, cleanRoot+string(os.PathSeparator)) {
		return fmt.Errorf("automation checkout archive target escapes destination")
	}
	return nil
}

func incrementAutomationCheckoutFiles(result *runtime.CheckoutResult, maxFiles int) error {
	if maxFiles > 0 && result.Files+1 > maxFiles {
		return fmt.Errorf("automation checkout archive exceeds %d files", maxFiles)
	}
	result.Files++
	return nil
}

func writeAutomationCheckoutFile(ctx context.Context, root string, target string, relPath string, header *tar.Header, reader io.Reader, verifiedParents map[string]struct{}) error {
	if err := ensureNoCheckoutSymlinkAncestor(root, target, verifiedParents); err != nil {
		return err
	}
	if info, err := os.Lstat(target); err == nil {
		if info.Mode()&os.ModeSymlink != 0 {
			return fmt.Errorf("automation checkout archive refuses to overwrite symlink %q", relPath)
		}
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("inspect automation checkout file %q: %w", relPath, err)
	}
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return fmt.Errorf("create automation checkout parent for %q: %w", relPath, err)
	}
	verifiedParents[filepath.Clean(filepath.Dir(target))] = struct{}{}
	file, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, checkoutFileMode(header))
	if err != nil {
		return fmt.Errorf("create automation checkout file %q: %w", relPath, err)
	}
	copied, copyErr := io.Copy(file, contextReader{ctx: ctx, reader: reader})
	closeErr := file.Close()
	if copyErr != nil {
		return fmt.Errorf("write automation checkout file %q: %w", relPath, copyErr)
	}
	if closeErr != nil {
		return fmt.Errorf("close automation checkout file %q: %w", relPath, closeErr)
	}
	if err := os.Chmod(target, checkoutFileMode(header)); err != nil {
		return fmt.Errorf("set automation checkout file mode %q: %w", relPath, err)
	}
	if copied != header.Size {
		return fmt.Errorf("automation checkout file %q size mismatch", relPath)
	}
	return nil
}

func createAutomationCheckoutSymlink(root string, target string, relPath string, linkName string, verifiedParents map[string]struct{}) error {
	if err := ensureNoCheckoutSymlinkAncestor(root, target, verifiedParents); err != nil {
		return err
	}
	if err := validateAutomationCheckoutLinkTarget(relPath, linkName); err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return fmt.Errorf("create automation checkout symlink parent for %q: %w", relPath, err)
	}
	verifiedParents[filepath.Clean(filepath.Dir(target))] = struct{}{}
	if info, err := os.Lstat(target); err == nil {
		if info.IsDir() {
			return fmt.Errorf("automation checkout archive refuses to replace directory %q with symlink", relPath)
		}
		if err := os.Remove(target); err != nil {
			return fmt.Errorf("replace automation checkout symlink %q: %w", relPath, err)
		}
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("inspect automation checkout symlink %q: %w", relPath, err)
	}
	if err := os.Symlink(linkName, target); err != nil {
		return fmt.Errorf("create automation checkout symlink %q: %w", relPath, err)
	}
	return nil
}

func validateAutomationCheckoutLinkTarget(relPath string, linkName string) error {
	link := filepath.ToSlash(linkName)
	if link == "" || strings.HasPrefix(link, "/") || path.IsAbs(link) {
		return fmt.Errorf("automation checkout archive contains unsafe symlink %q", relPath)
	}
	resolved := path.Clean(path.Join(path.Dir(relPath), link))
	if resolved == "." || resolved == ".." || strings.HasPrefix(resolved, "../") {
		return fmt.Errorf("automation checkout archive symlink %q escapes destination", relPath)
	}
	if _, err := automationCheckoutArchivePath(resolved); err != nil {
		return fmt.Errorf("automation checkout archive contains unsafe symlink %q", relPath)
	}
	return nil
}

func ensureNoCheckoutSymlinkAncestor(root string, target string, verifiedParents map[string]struct{}) error {
	dir := filepath.Dir(filepath.Clean(target))
	root = filepath.Clean(root)
	rel, err := filepath.Rel(root, dir)
	if err != nil {
		return fmt.Errorf("inspect automation checkout path: %w", err)
	}
	if rel == "." {
		return nil
	}
	current := root
	for _, segment := range strings.Split(rel, string(os.PathSeparator)) {
		current = filepath.Join(current, segment)
		if _, ok := verifiedParents[current]; ok {
			continue
		}
		info, err := os.Lstat(current)
		if os.IsNotExist(err) {
			continue
		}
		if err != nil {
			return fmt.Errorf("inspect automation checkout parent %q: %w", current, err)
		}
		if info.Mode()&os.ModeSymlink != 0 {
			return fmt.Errorf("automation checkout archive path uses symlink parent")
		}
		if !info.IsDir() {
			return fmt.Errorf("automation checkout archive path parent is not a directory")
		}
		verifiedParents[current] = struct{}{}
	}
	return nil
}

func checkoutFileMode(header *tar.Header) os.FileMode {
	mode := os.FileMode(header.Mode).Perm()
	if mode == 0 {
		return 0o644
	}
	return mode
}

func checkoutDirMode(header *tar.Header) os.FileMode {
	mode := os.FileMode(header.Mode).Perm()
	if mode == 0 {
		return 0o755
	}
	return mode
}

type contextReader struct {
	ctx    context.Context
	reader io.Reader
}

func (r contextReader) Read(payload []byte) (int, error) {
	if err := r.ctx.Err(); err != nil {
		return 0, err
	}
	return r.reader.Read(payload)
}
