package server

import (
	"bytes"
	"context"
	"fmt"
	"strings"
	"sync"
)

const repositoryMirrorPushOutputLimit = 1024

// RepositoryMirrorPushRequest describes a trusted internal mirror transfer.
// The target is an AGS repository identity; its filesystem location remains
// owned by AGS.
type RepositoryMirrorPushRequest struct {
	RepositoryFullName string
	SourceGitDir       string
	RefSpecs           []string
}

// PushRepositoryMirror transfers authoritative mirror refs into an AGS-owned
// repository with native Git. The embedding host must complete product
// authorization before calling this privileged, process-local capability.
// Receive hooks on the target repository still run; --no-verify only disables
// source-side push hooks. The caller owns mirror-completion projections, so
// this primitive intentionally does not dispatch public smart-HTTP post-push
// integrations.
func (s *Server) PushRepositoryMirror(ctx context.Context, request RepositoryMirrorPushRequest) error {
	if ctx == nil {
		ctx = context.Background()
	}
	if s == nil || s.deps == nil || s.deps.Store == nil || s.deps.SvcDeps == nil {
		return fmt.Errorf("ags server is not initialized")
	}
	fullName := strings.TrimSpace(request.RepositoryFullName)
	if fullName == "" {
		return fmt.Errorf("repository full name is required")
	}
	sourceGitDir := request.SourceGitDir
	if strings.TrimSpace(sourceGitDir) == "" {
		return fmt.Errorf("source Git directory is required")
	}
	if len(request.RefSpecs) == 0 {
		return fmt.Errorf("at least one mirror refspec is required")
	}
	refSpecs := make([]string, 0, len(request.RefSpecs))
	for _, refSpec := range request.RefSpecs {
		refSpec = strings.TrimSpace(refSpec)
		if refSpec == "" {
			return fmt.Errorf("mirror refspec is required")
		}
		refSpecs = append(refSpecs, refSpec)
	}
	repository, err := s.deps.SvcDeps.LookupRepoIdentity(ctx, fullName)
	if err != nil {
		return fmt.Errorf("resolve mirror target identity: %w", err)
	}
	targetGitDir, err := s.deps.Store.GetRepoPath(ctx, repository.FullName)
	if err != nil {
		return fmt.Errorf("resolve mirror target repository: %w", err)
	}

	output := newBoundedRepositoryMirrorOutput(sourceGitDir, targetGitDir)
	err = s.deps.SvcDeps.PushRepositoryMirror(ctx, repository.ID, repository.FullName, sourceGitDir, refSpecs, output.Writer())
	if err == nil {
		return nil
	}
	if ctxErr := ctx.Err(); ctxErr != nil {
		return fmt.Errorf("push repository mirror: %w", ctxErr)
	}
	message := output.String()
	if message == "" {
		return fmt.Errorf("push repository mirror: %w", err)
	}
	return fmt.Errorf("push repository mirror: %w: %s", err, message)
}

type repositoryMirrorRedaction struct {
	secret      []byte
	replacement []byte
}

// boundedRepositoryMirrorOutput owns the shared bounded diagnostic buffer.
// Each child stream gets an independent redactor so stdout/stderr interleaving
// cannot expose a sensitive path split across writes on one stream.
type boundedRepositoryMirrorOutput struct {
	buffer     bytes.Buffer
	remaining  int
	redactions []repositoryMirrorRedaction
	streams    []*repositoryMirrorOutputStream
	mu         sync.Mutex
}

type repositoryMirrorOutputStream struct {
	output  *boundedRepositoryMirrorOutput
	pending []byte
}

func newBoundedRepositoryMirrorOutput(sourceGitDir, targetGitDir string) *boundedRepositoryMirrorOutput {
	redactions := make([]repositoryMirrorRedaction, 0, 2)
	if sourceGitDir != "" {
		redactions = append(redactions, repositoryMirrorRedaction{secret: []byte(sourceGitDir), replacement: []byte("[source]")})
	}
	if targetGitDir != "" && targetGitDir != sourceGitDir {
		redactions = append(redactions, repositoryMirrorRedaction{secret: []byte(targetGitDir), replacement: []byte("[target]")})
	}
	return &boundedRepositoryMirrorOutput{remaining: repositoryMirrorPushOutputLimit, redactions: redactions}
}

func (output *boundedRepositoryMirrorOutput) Writer() *repositoryMirrorOutputStream {
	stream := &repositoryMirrorOutputStream{output: output}
	output.mu.Lock()
	output.streams = append(output.streams, stream)
	output.mu.Unlock()
	return stream
}

func (stream *repositoryMirrorOutputStream) Write(value []byte) (int, error) {
	original := len(value)
	for _, next := range value {
		if stream.output.full() {
			stream.pending = stream.pending[:0]
			break
		}
		stream.pending = append(stream.pending, next)
		stream.flush(false)
	}
	return original, nil
}

// flush retains only a suffix that could still become a sensitive token on a
// later Write. Safe bytes and replacements enter the shared limit immediately;
// at EOF an incomplete sensitive prefix is redacted rather than disclosed.
func (stream *repositoryMirrorOutputStream) flush(final bool) {
	for len(stream.pending) != 0 {
		if stream.output.full() {
			stream.pending = stream.pending[:0]
			return
		}
		complete := -1
		potential := false
		for index, redaction := range stream.output.redactions {
			if len(stream.pending) < len(redaction.secret) && bytes.HasPrefix(redaction.secret, stream.pending) {
				potential = true
			}
			if bytes.HasPrefix(stream.pending, redaction.secret) && (complete < 0 || len(redaction.secret) > len(stream.output.redactions[complete].secret)) {
				complete = index
			}
		}
		if !final && potential {
			return
		}
		if complete >= 0 {
			redaction := stream.output.redactions[complete]
			stream.output.append(redaction.replacement)
			stream.pending = stream.pending[len(redaction.secret):]
			continue
		}
		if final && potential {
			stream.output.append([]byte("[path]"))
			stream.pending = stream.pending[:0]
			return
		}
		stream.output.append(stream.pending[:1])
		stream.pending = stream.pending[1:]
	}
}

func (output *boundedRepositoryMirrorOutput) append(value []byte) {
	output.mu.Lock()
	defer output.mu.Unlock()
	if len(value) > output.remaining {
		value = value[:output.remaining]
	}
	_, _ = output.buffer.Write(value)
	output.remaining -= len(value)
}

func (output *boundedRepositoryMirrorOutput) full() bool {
	output.mu.Lock()
	defer output.mu.Unlock()
	return output.remaining == 0
}

func (output *boundedRepositoryMirrorOutput) String() string {
	for _, stream := range output.streams {
		stream.flush(true)
	}
	output.mu.Lock()
	defer output.mu.Unlock()
	return strings.TrimSpace(strings.ToValidUTF8(output.buffer.String(), "?"))
}
