package service

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"
	"unicode"

	"sproutworkcollect-backend/internal/config"
	"sproutworkcollect-backend/internal/model"
)

// WorkService manages all work data operations with concurrent-safe file I/O.
type WorkService struct {
	cfg *config.Config
	mu  sync.RWMutex
}

// NewWorkService creates a WorkService with the given config.
func NewWorkService(cfg *config.Config) *WorkService {
	return &WorkService{cfg: cfg}
}

// WorksDir returns the configured works root directory.
func (s *WorkService) WorksDir() string { return s.cfg.WorksDir }

// WorkExists reports whether a work directory is present on disk.
func (s *WorkService) WorkExists(workID string) bool {
	_, err := os.Stat(filepath.Join(s.cfg.WorksDir, workID))
	return err == nil
}

func isExternalURL(value string) bool {
	trimmed := strings.TrimSpace(value)
	return strings.HasPrefix(trimmed, "http://") || strings.HasPrefix(trimmed, "https://")
}

// ─── Read operations ──────────────────────────────────────────────────────────

// LoadWork loads and returns a single work config from disk (read-locked).
func (s *WorkService) LoadWork(workID string) (*model.WorkConfig, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.loadWork(workID)
}

// loadWork is the internal (unlocked) loader; callers must hold at least an RLock.
func (s *WorkService) loadWork(workID string) (*model.WorkConfig, error) {
	path := filepath.Join(s.cfg.WorksDir, workID, "work_config.json")
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var work model.WorkConfig
	if err := json.Unmarshal(data, &work); err != nil {
		return nil, err
	}
	work.Normalize()
	return &work, nil
}

// LoadAllWorks loads every work and returns them sorted by UpdateTime descending.
func (s *WorkService) LoadAllWorks() ([]*model.WorkConfig, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	entries, err := os.ReadDir(s.cfg.WorksDir)
	if err != nil {
		if os.IsNotExist(err) {
			return []*model.WorkConfig{}, nil
		}
		return nil, err
	}

	var works []*model.WorkConfig
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		w, err := s.loadWork(e.Name())
		if err != nil {
			continue // skip broken configs
		}
		works = append(works, w)
	}

	sort.Slice(works, func(i, j int) bool {
		return works[i].UpdateTime > works[j].UpdateTime
	})
	return works, nil
}

// SearchWorks filters all works by keyword (name / desc / tags) and/or category.
func (s *WorkService) SearchWorks(query, category string) ([]*model.WorkConfig, error) {
	all, err := s.LoadAllWorks()
	if err != nil {
		return nil, err
	}

	q := strings.ToLower(query)
	var result []*model.WorkConfig
	for _, w := range all {
		if q != "" {
			matched := strings.Contains(strings.ToLower(w.WorkName), q) ||
				strings.Contains(strings.ToLower(w.WorkDesc), q)
			if !matched {
				for _, tag := range w.Tags {
					if strings.Contains(strings.ToLower(tag), q) {
						matched = true
						break
					}
				}
			}
			if !matched {
				continue
			}
		}
		if category != "" && w.Category != category {
			continue
		}
		result = append(result, w)
	}
	return result, nil
}

// AllCategories returns a deduplicated list of all work categories.
func (s *WorkService) AllCategories() ([]string, error) {
	all, err := s.LoadAllWorks()
	if err != nil {
		return nil, err
	}
	seen := make(map[string]struct{})
	var cats []string
	for _, w := range all {
		if w.Category != "" {
			if _, ok := seen[w.Category]; !ok {
				seen[w.Category] = struct{}{}
				cats = append(cats, w.Category)
			}
		}
	}
	if cats == nil {
		cats = []string{}
	}
	return cats, nil
}

// BuildResponse attaches dynamically computed link fields to a work for API responses.
// These fields are never written back to disk.
func (s *WorkService) BuildResponse(w *model.WorkConfig) *model.WorkResponse {
	resp := &model.WorkResponse{
		WorkConfig:         *w,
		DownloadLinks:      make(map[string][]string),
		DownloadResources:  make(map[string][]model.DownloadResource),
	}

	for _, platform := range w.Platforms {
		if files, ok := w.FileNames[platform]; ok && len(files) > 0 {
			links := make([]string, len(files))
			for i, f := range files {
				rel := fmt.Sprintf("/api/download/%s/%s/%s", w.WorkID, platform, f)
				links[i] = rel
				resp.DownloadResources[platform] = append(resp.DownloadResources[platform], model.DownloadResource{
					Type:  "local",
					Alias: f,
					URL:   rel,
				})
			}
			resp.DownloadLinks[platform] = links
		}

		// 外部下载链接（带别名）
		if extList, ok := w.ExternalDownloads[platform]; ok && len(extList) > 0 {
			for _, item := range extList {
				if strings.TrimSpace(item.URL) == "" {
					continue
				}
				alias := strings.TrimSpace(item.Alias)
				if alias == "" {
					alias = "外部下载"
				}
				resp.DownloadResources[platform] = append(resp.DownloadResources[platform], model.DownloadResource{
					Type:  "external",
					Alias: alias,
					URL:   strings.TrimSpace(item.URL),
				})
			}
		}
	}

	if len(w.Screenshots) > 0 {
		resp.ImageLinks = make([]string, len(w.Screenshots))
		for i, img := range w.Screenshots {
			if isExternalURL(img) {
				resp.ImageLinks[i] = strings.TrimSpace(img)
			} else {
				resp.ImageLinks[i] = fmt.Sprintf("/api/image/%s/%s", w.WorkID, img)
			}
		}
	}

	if len(w.VideoFiles) > 0 {
		resp.VideoLinks = make([]string, len(w.VideoFiles))
		for i, vid := range w.VideoFiles {
			if isExternalURL(vid) {
				resp.VideoLinks[i] = strings.TrimSpace(vid)
			} else {
				resp.VideoLinks[i] = fmt.Sprintf("/api/video/%s/%s", w.WorkID, vid)
			}
		}
	}

	return resp
}

// ─── Write operations ─────────────────────────────────────────────────────────

// SaveWork atomically persists a work config (write-locked).
// It writes to a .tmp file first, then renames to guarantee atomicity.
func (s *WorkService) SaveWork(work *model.WorkConfig) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.saveWork(work)
}

// saveWork is the internal (unlocked) writer; callers must hold the write lock.
func (s *WorkService) saveWork(work *model.WorkConfig) error {
	configPath := filepath.Join(s.cfg.WorksDir, work.WorkID, "work_config.json")
	data, err := json.MarshalIndent(work, "", "  ")
	if err != nil {
		return fmt.Errorf("序列化配置失败: %w", err)
	}
	tmp := configPath + ".tmp"
	if err := os.WriteFile(tmp, data, 0644); err != nil {
		return fmt.Errorf("写入临时文件失败: %w", err)
	}
	if err := os.Rename(tmp, configPath); err != nil {
		_ = os.Remove(tmp)
		return fmt.Errorf("原子写入失败: %w", err)
	}
	return nil
}

// ModifyWork loads a work under a write lock, applies fn, then saves the result.
// Using this helper avoids the load–modify–save TOCTOU race for concurrent requests.
func (s *WorkService) ModifyWork(workID string, fn func(*model.WorkConfig)) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	work, err := s.loadWork(workID)
	if err != nil {
		return err
	}
	fn(work)
	work.UpdateTime = now()
	return s.saveWork(work)
}

// UpdateStats increments a statistical counter ("view" | "download" | "like").
func (s *WorkService) UpdateStats(workID, statType string) error {
	return s.ModifyWork(workID, func(w *model.WorkConfig) {
		switch statType {
		case "view":
			w.Views++
		case "download":
			w.Downloads++
		case "like":
			w.Likes++
		}
	})
}

// CreateWork initialises a new work directory tree and writes the first config.
func (s *WorkService) CreateWork(work *model.WorkConfig) error {
	workDir := filepath.Join(s.cfg.WorksDir, work.WorkID)
	if _, err := os.Stat(workDir); err == nil {
		return fmt.Errorf("作品ID已存在")
	}

	dirs := []string{
		workDir,
		filepath.Join(workDir, "image"),
		filepath.Join(workDir, "video"),
		filepath.Join(workDir, "platform"),
	}
	for _, p := range work.Platforms {
		dirs = append(dirs, filepath.Join(workDir, "platform", p))
	}
	for _, d := range dirs {
		if err := os.MkdirAll(d, 0755); err != nil {
			return fmt.Errorf("创建目录失败 %s: %w", d, err)
		}
	}
	return s.SaveWork(work)
}

// UpdateWork merges incoming data into the existing config, preserving statistics
// when the caller does not provide them (zero values).
func (s *WorkService) UpdateWork(workID string, incoming *model.WorkConfig) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	existing, err := s.loadWork(workID)
	if err != nil {
		return fmt.Errorf("作品不存在: %w", err)
	}

	// 兼容旧前端：如果某些字段未带上，则保留旧值，避免被清空。
	if incoming.FileNames == nil {
		incoming.FileNames = existing.FileNames
	}
	if incoming.ExternalDownloads == nil {
		incoming.ExternalDownloads = existing.ExternalDownloads
	}
	if incoming.Screenshots == nil {
		incoming.Screenshots = existing.Screenshots
	}
	if incoming.VideoFiles == nil {
		incoming.VideoFiles = existing.VideoFiles
	}
	if incoming.Cover == "" {
		incoming.Cover = existing.Cover
	}
	if incoming.OriginalNames == nil {
		incoming.OriginalNames = existing.OriginalNames
	}

	if incoming.Downloads == 0 {
		incoming.Downloads = existing.Downloads
	}
	if incoming.Views == 0 {
		incoming.Views = existing.Views
	}
	if incoming.Likes == 0 {
		incoming.Likes = existing.Likes
	}
	if incoming.UploadTime == "" {
		incoming.UploadTime = existing.UploadTime
	}

	incoming.WorkID = workID
	incoming.UpdateTime = now()
	incoming.UpdateCount = existing.UpdateCount + 1
	incoming.Normalize()

	return s.saveWork(incoming)
}

// DeleteWork removes the entire work directory.
func (s *WorkService) DeleteWork(workID string) error {
	workDir := filepath.Join(s.cfg.WorksDir, workID)
	if _, err := os.Stat(workDir); os.IsNotExist(err) {
		return fmt.Errorf("作品不存在")
	}
	return os.RemoveAll(workDir)
}

// ─── Utility helpers ──────────────────────────────────────────────────────────

// SafeFilename sanitises a filename while preserving CJK (Chinese) characters.
func SafeFilename(filename string) string {
	if filename == "" {
		return "unnamed_file"
	}
	var sb strings.Builder
	for _, r := range filename {
		switch {
		case r >= 0x4E00 && r <= 0x9FFF: // CJK Unified Ideographs
			sb.WriteRune(r)
		case unicode.IsLetter(r) || unicode.IsDigit(r):
			sb.WriteRune(r)
		case r == '-' || r == '_' || r == '.' || r == ' ':
			sb.WriteRune(r)
		}
	}
	safe := strings.ReplaceAll(sb.String(), " ", "_")
	safe = strings.Trim(safe, "._")
	if safe == "" {
		return "unnamed_file"
	}
	return safe
}

// UniqueFilename returns a filename that does not appear in existing.
// If base already conflicts, it appends _1, _2, … until unique.
func UniqueFilename(base string, existing []string) string {
	set := make(map[string]bool, len(existing))
	for _, f := range existing {
		set[f] = true
	}
	if !set[base] {
		return base
	}
	ext := filepath.Ext(base)
	stem := strings.TrimSuffix(base, ext)
	for i := 1; ; i++ {
		name := fmt.Sprintf("%s_%d%s", stem, i, ext)
		if !set[name] {
			return name
		}
	}
}

// ContainsString reports whether slice s contains value v.
func ContainsString(s []string, v string) bool {
	for _, item := range s {
		if item == v {
			return true
		}
	}
	return false
}

// RemoveString returns a copy of s with the first occurrence of v removed.
func RemoveString(s []string, v string) []string {
	out := make([]string, 0, len(s))
	for _, item := range s {
		if item != v {
			out = append(out, item)
		}
	}
	return out
}

// now returns the current local time in Python-compatible ISO8601 format.
func now() string {
	return time.Now().Format("2006-01-02T15:04:05.000000")
}
