package service

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/minio/minio-go/v7"

	"git.iioio.com/freefire/jiang13-forum/config"
	"git.iioio.com/freefire/jiang13-forum/model"
)

// MediaItem 管理端媒体资源条目
type MediaItem struct {
	Category    string    `json:"category"`
	Name        string    `json:"name"`
	URL         string    `json:"url"`
	Size        int64     `json:"size"`
	ModifiedAt  time.Time `json:"modified_at"`
	ContentType string    `json:"content_type"`
	StorageType string    `json:"storage_type,omitempty"`
}

// MediaListResult 媒体列表分页结果
type MediaListResult struct {
	Files          []MediaItem    `json:"files"`
	Total          int            `json:"total"`
	Page           int            `json:"page"`
	TotalPages     int            `json:"total_pages"`
	StorageType    string         `json:"storage_type"`
	CategoryCounts map[string]int `json:"category_counts"`
}

var mediaCategories = []string{
	UploadCategoryAvatars,
	UploadCategoryPosts,
	UploadCategorySite,
}

// ListMedia 从数据库索引列出媒体（上传/删除时维护；启动时会扫盘回填）
func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaListResult, error) {
	if s == nil {
		return nil, errors.New("上传存储未初始化")
	}
	if model.DB == nil {
		return nil, errors.New("数据库未初始化")
	}
	if page < 1 {
		page = 1
	}
	if size < 1 {
		size = 24
	}
	if size > 100 {
		size = 100
	}
	category = strings.ToLower(strings.TrimSpace(category))
	if category == "" || category == "all" {
		category = "all"
	} else if !validMediaCategory(category) {
		return nil, errors.New("无效的分类")
	}
	query = strings.TrimSpace(query)

	// 索引为空时先同步一次，避免升级后首次打开空白
	var indexed int64
	_ = model.DB.Model(&model.Media{}).Count(&indexed).Error
	if indexed == 0 {
		_, _ = s.SyncMediaIndex()
	}

	counts := map[string]int{
		UploadCategoryAvatars: 0,
		UploadCategoryPosts:   0,
		UploadCategorySite:    0,
	}
	type catCount struct {
		Category string
		Cnt      int
	}
	var rows []catCount
	if err := model.DB.Model(&model.Media{}).
		Select("category, count(*) as cnt").
		Group("category").
		Scan(&rows).Error; err != nil {
		return nil, err
	}
	for _, r := range rows {
		counts[r.Category] = r.Cnt
	}

	dbq := model.DB.Model(&model.Media{})
	if category != "all" {
		dbq = dbq.Where("category = ?", category)
	}
	if query != "" {
		like := "%" + query + "%"
		dbq = dbq.Where("name LIKE ? OR url LIKE ?", like, like)
	}

	var total int64
	if err := dbq.Count(&total).Error; err != nil {
		return nil, err
	}
	totalPages := 1
	if total > 0 {
		totalPages = int((total + int64(size) - 1) / int64(size))
	}
	if page > totalPages {
		page = totalPages
	}

	var records []model.Media
	offset := (page - 1) * size
	if err := dbq.Order("created_at desc, id desc").Offset(offset).Limit(size).Find(&records).Error; err != nil {
		return nil, err
	}

	files := make([]MediaItem, 0, len(records))
	for _, r := range records {
		mod := r.UpdatedAt
		if mod.IsZero() {
			mod = r.CreatedAt
		}
		files = append(files, MediaItem{
			Category:    r.Category,
			Name:        r.Name,
			URL:         r.URL,
			Size:        r.Size,
			ModifiedAt:  mod.UTC(),
			ContentType: r.ContentType,
			StorageType: r.StorageType,
		})
	}

	mode, _, _, _ := s.snapshot()
	storageType := config.StorageTypeLocal
	if mode == config.StorageTypeS3 {
		storageType = config.StorageTypeS3
	}

	return &MediaListResult{
		Files:          files,
		Total:          int(total),
		Page:           page,
		TotalPages:     totalPages,
		StorageType:    storageType,
		CategoryCounts: counts,
	}, nil
}

// ListUserPostImages 列出当前用户历史上传的帖子图片（category=posts）
func (s *UploadStore) ListUserPostImages(userID uint, page, size int) (*MediaListResult, error) {
	if s == nil {
		return nil, errors.New("上传存储未初始化")
	}
	if model.DB == nil {
		return nil, errors.New("数据库未初始化")
	}
	if userID == 0 {
		return nil, errors.New("未登录")
	}
	if page < 1 {
		page = 1
	}
	if size < 1 {
		size = 24
	}
	if size > 100 {
		size = 100
	}

	var records []model.Media
	if err := model.DB.Where("category = ? AND user_id = ?", UploadCategoryPosts, userID).
		Order("created_at desc, id desc").
		Find(&records).Error; err != nil {
		return nil, err
	}

	// 上传会登记原图 + WebP；图库按 stem 去重，优先展示 WebP（与插入 URL 一致）
	records = dedupePostMediaPreferWebP(records)

	total := len(records)
	totalPages := 1
	if total > 0 {
		totalPages = (total + size - 1) / size
	}
	if page > totalPages {
		page = totalPages
	}
	start := (page - 1) * size
	if start > total {
		start = total
	}
	end := start + size
	if end > total {
		end = total
	}
	pageRecords := records[start:end]

	files := make([]MediaItem, 0, len(pageRecords))
	for _, r := range pageRecords {
		mod := r.UpdatedAt
		if mod.IsZero() {
			mod = r.CreatedAt
		}
		files = append(files, MediaItem{
			Category:    r.Category,
			Name:        r.Name,
			URL:         r.URL,
			Size:        r.Size,
			ModifiedAt:  mod.UTC(),
			ContentType: r.ContentType,
			StorageType: r.StorageType,
		})
	}

	mode, _, _, _ := s.snapshot()
	storageType := config.StorageTypeLocal
	if mode == config.StorageTypeS3 {
		storageType = config.StorageTypeS3
	}

	return &MediaListResult{
		Files:          files,
		Total:          total,
		Page:           page,
		TotalPages:     totalPages,
		StorageType:    storageType,
		CategoryCounts: map[string]int{UploadCategoryPosts: total},
	}, nil
}

// dedupePostMediaPreferWebP 同一上传的原图/WebP 只保留一条，优先 WebP
func dedupePostMediaPreferWebP(records []model.Media) []model.Media {
	type slot struct {
		idx    int
		isWebP bool
	}
	seen := map[string]slot{}
	out := make([]model.Media, 0, len(records))
	for _, r := range records {
		stem := mediaFileStem(r.Name)
		isWebP := strings.EqualFold(filepath.Ext(r.Name), ".webp") ||
			strings.EqualFold(r.ContentType, "image/webp")
		if s, ok := seen[stem]; ok {
			if isWebP && !s.isWebP {
				out[s.idx] = r
				seen[stem] = slot{idx: s.idx, isWebP: true}
			}
			continue
		}
		seen[stem] = slot{idx: len(out), isWebP: isWebP}
		out = append(out, r)
	}
	return out
}

func mediaFileStem(name string) string {
	name = filepath.Base(strings.TrimSpace(name))
	ext := filepath.Ext(name)
	if ext == "" {
		return strings.ToLower(name)
	}
	return strings.ToLower(strings.TrimSuffix(name, ext))
}

// DeleteMedia 按 URL 批量删除媒体（含伴生扩展名与数据库索引）
func (s *UploadStore) DeleteMedia(urls []string) (int, error) {
	if s == nil {
		return 0, errors.New("上传存储未初始化")
	}
	n := 0
	seen := map[string]bool{}
	for _, u := range urls {
		u = strings.TrimSpace(u)
		if u == "" || seen[u] {
			continue
		}
		seen[u] = true
		if !s.isManagedMediaURL(u) {
			continue
		}
		s.DeleteByURL(u)
		n++
	}
	return n, nil
}

// SyncMediaIndex 扫描当前存储后端，回填/校正媒体索引；返回写入或更新条数
func (s *UploadStore) SyncMediaIndex() (int, error) {
	if s == nil || model.DB == nil {
		return 0, errors.New("存储或数据库未初始化")
	}
	mode, _, _, _ := s.snapshot()
	storageType := config.StorageTypeLocal
	var items []MediaItem
	var err error
	if mode == config.StorageTypeS3 {
		storageType = config.StorageTypeS3
		items, err = s.listMediaS3("all")
	} else {
		items, err = s.listMediaLocal("all")
	}
	if err != nil {
		return 0, err
	}

	seen := make(map[string]struct{}, len(items))
	n := 0
	for _, it := range items {
		seen[it.URL] = struct{}{}
		if err := s.upsertMediaRecord(it.Category, it.Name, it.URL, it.Size, it.ContentType, storageType, nil); err != nil {
			continue
		}
		n++
	}

	// 清理当前后端下已不存在的索引（其它后端记录保留）
	var stale []model.Media
	_ = model.DB.Where("storage_type = ?", storageType).Find(&stale).Error
	for _, row := range stale {
		if _, ok := seen[row.URL]; ok {
			continue
		}
		_ = model.DB.Delete(&model.Media{}, row.ID).Error
	}
	return n, nil
}

func (s *UploadStore) upsertMediaRecord(category, name, url string, size int64, contentType, storageType string, userID *uint) error {
	if model.DB == nil || strings.TrimSpace(url) == "" {
		return nil
	}
	category = strings.TrimSpace(category)
	name = strings.TrimSpace(name)
	url = strings.TrimSpace(url)
	if storageType == "" {
		storageType = config.StorageTypeLocal
	}
	if contentType == "" {
		contentType = imageContentType(strings.ToLower(filepath.Ext(name)))
	}

	var existing model.Media
	err := model.DB.Where("url = ?", url).First(&existing).Error
	if err == nil {
		updates := map[string]interface{}{
			"category":     category,
			"name":         name,
			"size":         size,
			"content_type": contentType,
			"storage_type": storageType,
		}
		if userID != nil {
			updates["user_id"] = *userID
		}
		return model.DB.Model(&existing).Updates(updates).Error
	}

	rec := model.Media{
		Category:    category,
		Name:        name,
		URL:         url,
		Size:        size,
		ContentType: contentType,
		StorageType: storageType,
		UserID:      userID,
	}
	return model.DB.Create(&rec).Error
}

func (s *UploadStore) deleteMediaRecords(urls []string) {
	if model.DB == nil || len(urls) == 0 {
		return
	}
	clean := make([]string, 0, len(urls))
	seen := map[string]bool{}
	for _, u := range urls {
		u = strings.TrimSpace(u)
		if u == "" || seen[u] {
			continue
		}
		seen[u] = true
		clean = append(clean, u)
	}
	if len(clean) == 0 {
		return
	}
	_ = model.DB.Where("url IN ?", clean).Delete(&model.Media{}).Error
}

func (s *UploadStore) resolveSiblingPublicURLs(rawURL string) []string {
	rawURL = strings.TrimSpace(rawURL)
	if rawURL == "" {
		return nil
	}
	var rel string
	var basePrefix string // 拼回公开 URL 的前缀（含 category 前的部分）

	if strings.HasPrefix(rawURL, "/uploads/") {
		path := rawURL
		if i := strings.Index(path, "?"); i >= 0 {
			path = path[:i]
		}
		rel = strings.TrimPrefix(path, "/uploads/")
		basePrefix = "/uploads/"
	} else {
		_, publicBase, _, _ := s.snapshot()
		r, ok := relativeUnderPublicBase(rawURL, publicBase)
		if !ok {
			return []string{rawURL}
		}
		rel = r
		basePrefix = strings.TrimRight(publicBase, "/") + "/"
	}

	siblings := uploadSiblingRels(rel)
	if len(siblings) == 0 {
		return []string{rawURL}
	}
	out := make([]string, 0, len(siblings))
	for _, sib := range siblings {
		out = append(out, basePrefix+sib)
	}
	return out
}

func parseUploaderID(category, namePrefix string) *uint {
	if category != UploadCategoryAvatars && category != UploadCategoryPosts {
		return nil
	}
	id, err := strconv.ParseUint(strings.TrimSpace(namePrefix), 10, 64)
	if err != nil || id == 0 {
		return nil
	}
	u := uint(id)
	return &u
}

func (s *UploadStore) isManagedMediaURL(rawURL string) bool {
	rawURL = strings.TrimSpace(rawURL)
	if rawURL == "" {
		return false
	}
	if strings.HasPrefix(rawURL, "/uploads/") {
		rel := strings.TrimPrefix(rawURL, "/uploads/")
		cat, _, ok := splitCategoryName(rel)
		return ok && validMediaCategory(cat)
	}
	_, publicBase, _, backend := s.snapshot()
	if backend == nil || publicBase == "" {
		return false
	}
	rel, ok := relativeUnderPublicBase(rawURL, publicBase)
	if !ok {
		return false
	}
	cat, _, ok := splitCategoryName(rel)
	return ok && validMediaCategory(cat)
}

func (s *UploadStore) listMediaLocal(category string) ([]MediaItem, error) {
	cats := mediaCategories
	if category != "all" {
		cats = []string{category}
	}
	var out []MediaItem
	root := s.UploadsRoot()
	for _, cat := range cats {
		dir := filepath.Join(root, cat)
		entries, err := os.ReadDir(dir)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return nil, err
		}
		for _, e := range entries {
			if e.IsDir() {
				continue
			}
			name := e.Name()
			if strings.HasPrefix(name, ".") {
				continue
			}
			ext := strings.ToLower(filepath.Ext(name))
			if !allowedImageExt[ext] {
				continue
			}
			info, err := e.Info()
			if err != nil {
				continue
			}
			out = append(out, MediaItem{
				Category:    cat,
				Name:        name,
				URL:         "/uploads/" + cat + "/" + name,
				Size:        info.Size(),
				ModifiedAt:  info.ModTime().UTC(),
				ContentType: imageContentType(ext),
				StorageType: config.StorageTypeLocal,
			})
		}
	}
	return out, nil
}

func (s *UploadStore) listMediaS3(category string) ([]MediaItem, error) {
	_, publicBase, keyPrefix, backend := s.snapshot()
	if backend == nil {
		return nil, errors.New("对象存储未就绪")
	}
	cats := mediaCategories
	if category != "all" {
		cats = []string{category}
	}
	var out []MediaItem
	ctx := context.Background()
	for _, cat := range cats {
		prefix := keyPrefix + cat + "/"
		for obj := range backend.client.ListObjects(ctx, backend.bucket, minio.ListObjectsOptions{
			Prefix:    prefix,
			Recursive: true,
		}) {
			if obj.Err != nil {
				return nil, obj.Err
			}
			if strings.HasSuffix(obj.Key, "/") {
				continue
			}
			name := strings.TrimPrefix(obj.Key, prefix)
			if name == "" || strings.Contains(name, "/") {
				continue
			}
			if strings.HasPrefix(name, ".") {
				continue
			}
			ext := strings.ToLower(filepath.Ext(name))
			if !allowedImageExt[ext] {
				continue
			}
			out = append(out, MediaItem{
				Category:    cat,
				Name:        name,
				URL:         publicBase + "/" + cat + "/" + name,
				Size:        obj.Size,
				ModifiedAt:  obj.LastModified.UTC(),
				ContentType: imageContentType(ext),
				StorageType: config.StorageTypeS3,
			})
		}
	}
	return out, nil
}

func validMediaCategory(cat string) bool {
	switch cat {
	case UploadCategoryAvatars, UploadCategoryPosts, UploadCategorySite:
		return true
	default:
		return false
	}
}

func splitCategoryName(rel string) (category, name string, ok bool) {
	rel = strings.TrimSpace(strings.ReplaceAll(rel, "\\", "/"))
	rel = strings.TrimPrefix(rel, "/")
	parts := strings.SplitN(rel, "/", 2)
	if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
		return "", "", false
	}
	if strings.Contains(parts[1], "/") {
		return "", "", false
	}
	return parts[0], parts[1], true
}