package backend

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"unicode"
)

// ErrMediaExtractSourceTooLarge indicates that a staged source exceeded the
// limit supplied by the caller. The partially written task directory is
// removed by the caller's workspace cleanup.
var ErrMediaExtractSourceTooLarge = errors.New("media extract source exceeds configured max bytes")

// ErrMediaExtractWorkspaceRequired reports that a source is valid under the
// disk-backed source limit but is too large for the bounded in-memory fallback.
var ErrMediaExtractWorkspaceRequired = errors.New("media extract workspace required for source size")

// ErrMediaExtractDerivedTooLarge reports bounded derived artifacts exceeding
// their task budget.
var ErrMediaExtractDerivedTooLarge = errors.New("media extract derived output exceeds configured limit")

// DefaultMediaExtractMaxConcurrentTasks bounds process-local staging and
// media preprocessing without serializing all semantic workers by default.
const DefaultMediaExtractMaxConcurrentTasks = 4

// MediaExtractWorkspaceOptions configures the process-local media workspace.
// RootDir is intentionally optional so local development keeps using the
// platform temporary directory while production can mount a disk-backed path.
type MediaExtractWorkspaceOptions struct {
	RootDir            string
	MaxConcurrentTasks int
}

// MediaExtractWorkspace owns task-scoped temporary directories for media
// extraction. It does not retain media bytes in memory and is safe for use by
// concurrent semantic workers.
type MediaExtractWorkspace struct {
	root  string
	slots chan struct{}
}

// NewMediaExtractWorkspace creates a workspace rooted at opts.RootDir, or at
// os.TempDir when no explicit directory is provided.
func NewMediaExtractWorkspace(opts MediaExtractWorkspaceOptions) (*MediaExtractWorkspace, error) {
	root := strings.TrimSpace(opts.RootDir)
	if root == "" {
		root = os.TempDir()
	}
	if err := os.MkdirAll(root, 0o700); err != nil {
		return nil, fmt.Errorf("create media extract workspace %q: %w", root, err)
	}
	info, err := os.Stat(root)
	if err != nil {
		return nil, fmt.Errorf("stat media extract workspace %q: %w", root, err)
	}
	if !info.IsDir() {
		return nil, fmt.Errorf("media extract workspace %q is not a directory", root)
	}
	maxConcurrentTasks := opts.MaxConcurrentTasks
	if maxConcurrentTasks <= 0 {
		maxConcurrentTasks = DefaultMediaExtractMaxConcurrentTasks
	}
	return &MediaExtractWorkspace{root: root, slots: make(chan struct{}, maxConcurrentTasks)}, nil
}

// RootDir returns the configured root directory.
func (w *MediaExtractWorkspace) RootDir() string {
	if w == nil {
		return ""
	}
	return w.root
}

// MediaExtractTaskWorkspace is a private directory for one claimed task.
type MediaExtractTaskWorkspace struct {
	dir     string
	release func()
	mu      sync.Mutex
	open    bool
}

// NewTask creates a private task directory. Task IDs are sanitized so callers
// cannot escape the configured workspace through path separators.
func (w *MediaExtractWorkspace) NewTask(ctx context.Context, taskID string) (*MediaExtractTaskWorkspace, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if w == nil || w.root == "" {
		return nil, errors.New("media extract workspace is not configured")
	}
	select {
	case w.slots <- struct{}{}:
	case <-ctx.Done():
		return nil, ctx.Err()
	}
	release := func() { <-w.slots }
	name := sanitizeMediaWorkspaceName(taskID)
	dir, err := os.MkdirTemp(w.root, name+"-")
	if err != nil {
		release()
		return nil, fmt.Errorf("create media task workspace: %w", err)
	}
	if err := os.Chmod(dir, 0o700); err != nil {
		_ = os.RemoveAll(dir)
		release()
		return nil, fmt.Errorf("set media task workspace permissions: %w", err)
	}
	return &MediaExtractTaskWorkspace{dir: dir, release: release, open: true}, nil
}

// Dir returns the task directory.
func (t *MediaExtractTaskWorkspace) Dir() string {
	if t == nil {
		return ""
	}
	return t.dir
}

// StageReader streams a bounded source into the task directory. The returned
// byte count is the number of bytes written.
func (t *MediaExtractTaskWorkspace) StageReader(ctx context.Context, name string, src io.Reader, maxBytes int64) (string, int64, error) {
	if err := ctx.Err(); err != nil {
		return "", 0, err
	}
	if t == nil || !t.isOpen() {
		return "", 0, errors.New("media task workspace is closed")
	}
	if src == nil {
		return "", 0, errors.New("media source is nil")
	}
	name = sanitizeMediaWorkspaceName(name)
	path := filepath.Join(t.dir, name)
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
	if err != nil {
		return "", 0, fmt.Errorf("create staged media source: %w", err)
	}
	defer func() { _ = f.Close() }()

	reader := io.Reader(src)
	if maxBytes > 0 {
		reader = io.LimitReader(src, maxBytes+1)
	}
	n, copyErr := copyWithContext(ctx, f, reader)
	if copyErr != nil {
		_ = os.Remove(path)
		return "", n, copyErr
	}
	if maxBytes > 0 && n > maxBytes {
		_ = os.Remove(path)
		return "", n, ErrMediaExtractSourceTooLarge
	}
	return path, n, nil
}

// Close removes the task directory. It is safe to call more than once.
func (t *MediaExtractTaskWorkspace) Close() error {
	if t == nil {
		return nil
	}
	t.mu.Lock()
	if !t.open {
		t.mu.Unlock()
		return nil
	}
	t.open = false
	dir := t.dir
	release := t.release
	t.release = nil
	t.mu.Unlock()
	var err error
	if dir != "" {
		if removeErr := os.RemoveAll(dir); removeErr != nil {
			err = fmt.Errorf("remove media task workspace: %w", removeErr)
		}
	}
	if release != nil {
		release()
	}
	return err
}

func (t *MediaExtractTaskWorkspace) isOpen() bool {
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.open
}

func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) (int64, error) {
	buf := make([]byte, 32*1024)
	var total int64
	for {
		if err := ctx.Err(); err != nil {
			return total, err
		}
		n, readErr := src.Read(buf)
		if n > 0 {
			written, writeErr := dst.Write(buf[:n])
			total += int64(written)
			if writeErr != nil {
				return total, writeErr
			}
			if written != n {
				return total, io.ErrShortWrite
			}
		}
		if readErr != nil {
			if errors.Is(readErr, io.EOF) {
				return total, nil
			}
			return total, readErr
		}
	}
}

func sanitizeMediaWorkspaceName(name string) string {
	name = strings.TrimSpace(name)
	if name == "" {
		return "media"
	}
	var b strings.Builder
	for _, r := range name {
		if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' || r == '.' {
			b.WriteRune(r)
		} else {
			b.WriteByte('_')
		}
	}
	if out := strings.Trim(b.String(), "."); out != "" {
		return out
	}
	return "media"
}
