// 本文件负责将剧集视频按清单写入 ZIP，仅打包原始媒体字节，不执行转码或拼接。
package videoarchive

import (
	"archive/zip"
	"context"
	"fmt"
	"io"
	"strings"
	"unicode"
)

const maxFilenameRunes = 180

// Entry 描述 ZIP 中单个视频的对象存储位置和归档名称。
type Entry struct {
	ObjectKey string
	Name      string
}

// Manifest 描述一个剧集视频压缩包的下载名称和文件清单。
type Manifest struct {
	Name    string
	Entries []Entry
}

// OpenObject 按对象键打开原始媒体流，调用方负责提供具体存储实现。
type OpenObject func(context.Context, string) (io.ReadCloser, error)

// Write 按清单顺序写入 ZIP，并确保每个对象流在复制后立即关闭。
func Write(ctx context.Context, target io.Writer, entries []Entry, open OpenObject) error {
	writer := zip.NewWriter(target)
	for _, entry := range entries {
		body, err := open(ctx, entry.ObjectKey)
		if err != nil {
			return fmt.Errorf("打开归档视频 %q 失败: %w", entry.Name, err)
		}
		header := &zip.FileHeader{Name: entry.Name, Method: zip.Deflate}
		header.SetMode(0o644)
		part, createErr := writer.CreateHeader(header)
		if createErr == nil {
			_, createErr = io.Copy(part, body)
		}
		closeErr := body.Close()
		if createErr != nil {
			return fmt.Errorf("写入归档视频 %q 失败: %w", entry.Name, createErr)
		}
		if closeErr != nil {
			return fmt.Errorf("关闭归档视频 %q 失败: %w", entry.Name, closeErr)
		}
	}
	if err := writer.Close(); err != nil {
		return fmt.Errorf("完成 ZIP 写入失败: %w", err)
	}
	return nil
}

// SafeFilename 清理文件系统不允许的字符并限制长度，避免归档名形成路径或无法保存。
func SafeFilename(value, fallback string) string {
	cleaned := strings.Map(func(r rune) rune {
		if unicode.IsControl(r) || strings.ContainsRune(`\/:*?"<>|`, r) {
			return -1
		}
		return r
	}, strings.TrimSpace(value))
	cleaned = strings.Trim(cleaned, ". ")
	if cleaned == "" {
		cleaned = fallback
	}
	runes := []rune(cleaned)
	if len(runes) > maxFilenameRunes {
		cleaned = string(runes[:maxFilenameRunes])
	}
	return cleaned
}
