package service

import (
	"archive/zip"
	"bytes"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"time"
	"unicode"
	"unicode/utf16"
	"unicode/utf8"

	dramapkg "juhe-factory/api/internal/drama"
	"juhe-factory/api/internal/model"

	"github.com/google/uuid"
	"github.com/richardlehane/mscfb"
	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/transform"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const maxDramaImportBytes = 3 * 1024 * 1024

var chapterTitlePattern = regexp.MustCompile(`(?im)^\s*(?:#+\s*|[*+-]\s*)?((?:第[零〇一二三四五六七八九十百千万两\d]+[章节回卷篇部集幕].*)|(?:chapter\s+\d+.*))\s*$`)

type DramaChapter struct {
	Title     string `json:"title"`
	Content   string `json:"content,omitempty"`
	CharCount int    `json:"char_count"`
}

type DramaImportPreview struct {
	ImportToken                     uuid.UUID      `json:"import_token"`
	HasChapters                     bool           `json:"has_chapters"`
	TotalCharacters                 int            `json:"total_characters"`
	Chapters                        []DramaChapter `json:"chapters"`
	SingleEpisodeConfirmationNeeded bool           `json:"single_episode_confirmation_required"`
}

func (s *Creative) PreviewDramaImport(userID, projectID uuid.UUID, filename string, data []byte, rawText string) (*DramaImportPreview, error) {
	if err := s.requirePremiumProject(userID, projectID); err != nil {
		return nil, err
	}
	if len(data) > maxDramaImportBytes || len([]byte(rawText)) > maxDramaImportBytes {
		return nil, errors.New("小说文件或文本不能超过3MB")
	}
	text, sourceType, err := parseDramaSource(filename, data, rawText)
	if err != nil {
		return nil, err
	}
	text = normalizeDramaText(text)
	if text == "" {
		return nil, errors.New("未读取到有效小说正文")
	}
	chapters := splitDramaChapters(text)
	metadata := make([]DramaChapter, 0, len(chapters))
	for _, chapter := range chapters {
		metadata = append(metadata, DramaChapter{Title: chapter.Title, CharCount: len([]rune(chapter.Content))})
	}
	previewJSON, _ := json.Marshal(map[string]any{"has_chapters": len(chapters) > 0, "chapters": metadata})
	hash := sha256.Sum256([]byte(text))
	session := model.DramaImportSession{
		ID: uuid.New(), UserID: userID, ProjectID: projectID, SourceType: sourceType,
		SourceFilename: cleanDramaFilename(filename), RawContent: text, ContentSHA256: hex.EncodeToString(hash[:]),
		PreviewData: previewJSON, ExpiresAt: time.Now().Add(30 * time.Minute),
	}
	if err := s.DB.Create(&session).Error; err != nil {
		return nil, err
	}
	return &DramaImportPreview{
		ImportToken: session.ID, HasChapters: len(chapters) > 0, TotalCharacters: len([]rune(text)),
		Chapters: metadata, SingleEpisodeConfirmationNeeded: len(chapters) == 0,
	}, nil
}

func (s *Creative) ConfirmDramaImport(userID, projectID, token uuid.UUID, chaptersPerEpisode, startEpisodeNo int, treatAsSingle bool) ([]model.ProjectEpisode, error) {
	if chaptersPerEpisode < 1 {
		chaptersPerEpisode = 1
	}
	if startEpisodeNo < 1 {
		return nil, errors.New("起始集数无效")
	}
	created := make([]model.ProjectEpisode, 0)
	err := s.DB.Transaction(func(tx *gorm.DB) error {
		var project model.CreativeProject
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").Where("id=? AND user_id=? AND deleted_at IS NULL", projectID, userID).Take(&project).Error; err != nil {
			return gorm.ErrRecordNotFound
		}
		var session model.DramaImportSession
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=? AND user_id=? AND project_id=?", token, userID, projectID).Take(&session).Error; err != nil {
			return gorm.ErrRecordNotFound
		}
		if session.ConsumedAt != nil || time.Now().After(session.ExpiresAt) {
			return errors.New("导入预览已失效，请重新上传")
		}
		chapters := splitDramaChapters(session.RawContent)
		if len(chapters) == 0 {
			if !treatAsSingle {
				return errors.New("需要确认将全部内容作为一集导入")
			}
			chapters = []DramaChapter{{Title: fmt.Sprintf("第%d集", startEpisodeNo), Content: session.RawContent, CharCount: len([]rune(session.RawContent))}}
			chaptersPerEpisode = 1
		}
		for offset, index := 0, 0; index < len(chapters); offset, index = offset+1, index+chaptersPerEpisode {
			end := index + chaptersPerEpisode
			if end > len(chapters) {
				end = len(chapters)
			}
			group := chapters[index:end]
			episodeNo := startEpisodeNo + offset
			name := fmt.Sprintf("第%d集", episodeNo)
			if len(group) == 1 && strings.TrimSpace(group[0].Title) != "" {
				name = group[0].Title
			}
			name = truncateRunes(strings.TrimSpace(name), 160)
			exists, err := episodeNameExists(tx, projectID, nil, name)
			if err != nil {
				return err
			}
			if exists {
				return errors.New("同一项目下不可有同名剧集")
			}
			parts := make([]string, 0, len(group))
			for _, chapter := range group {
				parts = append(parts, strings.TrimSpace(chapter.Content))
			}
			content := strings.TrimSpace(strings.Join(parts, "\n\n"))
			if err := dramapkg.ValidateEpisodeContent(content); err != nil {
				return fmt.Errorf("第%d集：%w", episodeNo, err)
			}
			episode := model.ProjectEpisode{ID: uuid.New(), ProjectID: projectID, EpisodeNo: episodeNo, Name: name, AudioSource: "video_audio", Status: "draft"}
			if err := tx.Create(&episode).Error; err != nil {
				return err
			}
			hash := sha256.Sum256([]byte(content))
			source := model.EpisodeSource{ID: uuid.New(), EpisodeID: episode.ID, Title: episode.Name, RawContent: content, CharCount: len([]rune(content)), SourceType: session.SourceType, SourceFilename: session.SourceFilename, ContentSHA256: hex.EncodeToString(hash[:])}
			if err := tx.Create(&source).Error; err != nil {
				return err
			}
			created = append(created, episode)
		}
		now := time.Now()
		return tx.Model(&session).Updates(map[string]any{"consumed_at": now, "raw_content": ""}).Error
	})
	return created, err
}

func (s *Creative) EpisodeSource(userID, projectID, episodeID uuid.UUID) (*model.EpisodeSource, error) {
	var source model.EpisodeSource
	result := s.DB.Table("episode_sources source").Joins("JOIN project_episodes episode ON episode.id=source.episode_id AND episode.deleted_at IS NULL").Joins("JOIN creative_projects project ON project.id=episode.project_id AND project.deleted_at IS NULL").Where("source.episode_id=? AND episode.project_id=? AND project.user_id=?", episodeID, projectID, userID).Limit(1).Find(&source)
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, gorm.ErrRecordNotFound
	}
	return &source, nil
}

func (s *Creative) SaveEpisodeSource(userID, projectID, episodeID uuid.UUID, content, title string) (*model.EpisodeSource, error) {
	content = normalizeDramaText(content)
	if err := dramapkg.ValidateEpisodeContent(content); err != nil {
		return nil, err
	}
	if err := s.requirePremiumEpisode(userID, projectID, episodeID); err != nil {
		return nil, err
	}
	hash := sha256.Sum256([]byte(content))
	source := model.EpisodeSource{ID: uuid.New(), EpisodeID: episodeID, Title: truncateRunes(strings.TrimSpace(title), 200), RawContent: content, CharCount: len([]rune(content)), SourceType: "pasted_text", ContentSHA256: hex.EncodeToString(hash[:])}
	err := s.DB.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "episode_id"}}, DoUpdates: clause.AssignmentColumns([]string{"title", "raw_content", "char_count", "source_type", "source_filename", "content_sha256", "updated_at"})}).Create(&source).Error
	return &source, err
}

func (s *Creative) requirePremiumProject(userID, projectID uuid.UUID) error {
	var count int64
	err := s.DB.Table("creative_projects").Where("id=? AND user_id=? AND project_type='premium_drama' AND deleted_at IS NULL", projectID, userID).Count(&count).Error
	if err != nil {
		return err
	}
	if count == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (s *Creative) requirePremiumEpisode(userID, projectID, episodeID uuid.UUID) error {
	var count int64
	err := s.DB.Table("project_episodes episode").Joins("JOIN creative_projects project ON project.id=episode.project_id").Where("episode.id=? AND episode.project_id=? AND episode.deleted_at IS NULL AND project.user_id=? AND project.project_type='premium_drama' AND project.deleted_at IS NULL", episodeID, projectID, userID).Count(&count).Error
	if err != nil {
		return err
	}
	if count == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func parseDramaSource(filename string, data []byte, rawText string) (string, string, error) {
	if strings.TrimSpace(rawText) != "" {
		if len(data) > 0 {
			return "", "", errors.New("文件和粘贴文本只能选择一种")
		}
		if looksLikeBinary([]byte(rawText)) {
			return "", "", errors.New("粘贴内容包含二进制控制字符")
		}
		if looksLikeHTMLOrScript([]byte(rawText)) {
			return "", "", errors.New("不支持HTML或脚本内容")
		}
		return rawText, "pasted_text", nil
	}
	if len(data) == 0 {
		return "", "", errors.New("请选择小说文件或粘贴文本")
	}
	ext := strings.ToLower(filepath.Ext(filename))
	switch ext {
	case ".txt":
		text, err := decodePlainText(data)
		return text, "txt", err
	case ".docx":
		text, err := extractDOCX(data)
		return text, "docx", err
	case ".doc":
		text, err := extractLegacyDOC(data)
		return text, "doc", err
	default:
		return "", "", errors.New("仅支持TXT、DOC和DOCX文件")
	}
}

func decodePlainText(data []byte) (string, error) {
	if hasDisallowedBinarySignature(data) || bytes.IndexByte(data, 0) >= 0 || looksLikeBinary(data) {
		return "", errors.New("TXT文件包含二进制内容")
	}
	if looksLikeHTMLOrScript(data) {
		return "", errors.New("不支持HTML或脚本文件")
	}
	data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})
	if utf8.Valid(data) {
		return string(data), nil
	}
	decoded, err := io.ReadAll(transform.NewReader(bytes.NewReader(data), simplifiedchinese.GB18030.NewDecoder()))
	if err != nil || !utf8.Valid(decoded) {
		return "", errors.New("TXT文件编码不支持，请使用UTF-8或GB18030")
	}
	return string(decoded), nil
}

func extractDOCX(data []byte) (string, error) {
	if len(data) < 4 || !bytes.Equal(data[:4], []byte{'P', 'K', 3, 4}) {
		return "", errors.New("DOCX文件结构无效")
	}
	reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		return "", errors.New("DOCX文件结构无效")
	}
	if len(reader.File) > 1000 {
		return "", errors.New("DOCX文件包含过多内容")
	}
	var document, contentTypes []byte
	var expanded uint64
	for _, file := range reader.File {
		expanded += file.UncompressedSize64
		if expanded > 20*1024*1024 {
			return "", errors.New("DOCX展开后内容过大")
		}
		name := strings.ToLower(strings.ReplaceAll(file.Name, "\\", "/"))
		if strings.Contains(name, "vbaproject") || strings.Contains(name, "activex") || strings.Contains(name, "embeddings/") {
			return "", errors.New("DOCX包含宏、ActiveX或嵌入对象")
		}
		if name == "[content_types].xml" {
			stream, openErr := file.Open()
			if openErr != nil {
				return "", openErr
			}
			contentTypes, err = io.ReadAll(io.LimitReader(stream, 1024*1024))
			stream.Close()
			if err != nil {
				return "", err
			}
		}
		if name == "word/document.xml" {
			stream, openErr := file.Open()
			if openErr != nil {
				return "", openErr
			}
			document, err = io.ReadAll(io.LimitReader(stream, 12*1024*1024))
			stream.Close()
			if err != nil {
				return "", err
			}
		}
	}
	contentTypeText := strings.ToLower(string(contentTypes))
	if len(document) == 0 || !strings.Contains(contentTypeText, "wordprocessingml.document.main+xml") || strings.Contains(contentTypeText, "macroenabled") {
		return "", errors.New("DOCX缺少Word正文结构")
	}
	decoder := xml.NewDecoder(bytes.NewReader(document))
	var output strings.Builder
	for {
		token, tokenErr := decoder.Token()
		if tokenErr == io.EOF {
			break
		}
		if tokenErr != nil {
			return "", errors.New("DOCX正文XML无效")
		}
		switch value := token.(type) {
		case xml.CharData:
			output.Write([]byte(value))
		case xml.EndElement:
			if value.Name.Local == "p" {
				output.WriteString("\n")
			} else if value.Name.Local == "tab" {
				output.WriteString("\t")
			}
		}
	}
	return output.String(), nil
}

func extractLegacyDOC(data []byte) (string, error) {
	magic := []byte{0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}
	if len(data) < len(magic) || !bytes.Equal(data[:len(magic)], magic) {
		return "", errors.New("DOC文件结构无效")
	}
	reader, err := mscfb.New(bytes.NewReader(data))
	if err != nil {
		return "", errors.New("DOC文件结构无效")
	}
	streams := map[string][]byte{}
	for entry, nextErr := reader.Next(); nextErr == nil; entry, nextErr = reader.Next() {
		name := strings.ToLower(entry.Name)
		if strings.Contains(name, "vba") || strings.Contains(name, "macros") || strings.Contains(name, "objectpool") {
			return "", errors.New("DOC包含宏或嵌入对象")
		}
		if name == "worddocument" || name == "0table" || name == "1table" {
			content, readErr := io.ReadAll(io.LimitReader(entry, 12*1024*1024))
			if readErr != nil {
				return "", readErr
			}
			streams[name] = content
		}
	}
	word := streams["worddocument"]
	if len(word) < 0x1AA || binary.LittleEndian.Uint16(word[:2]) != 0xA5EC {
		return "", errors.New("DOC缺少有效WordDocument流")
	}
	flags := binary.LittleEndian.Uint16(word[0x0A:0x0C])
	tableName := "0table"
	if flags&0x0200 != 0 {
		tableName = "1table"
	}
	table := streams[tableName]
	fcClx := int(binary.LittleEndian.Uint32(word[0x1A2:0x1A6]))
	lcbClx := int(binary.LittleEndian.Uint32(word[0x1A6:0x1AA]))
	text, parseErr := extractDOCPieces(word, table, fcClx, lcbClx)
	if parseErr != nil || strings.TrimSpace(text) == "" {
		return "", errors.New("DOC正文无法安全解析")
	}
	return text, nil
}

func extractDOCPieces(word, table []byte, offset, length int) (string, error) {
	if offset < 0 || length <= 0 || offset+length > len(table) {
		return "", errors.New("invalid CLX")
	}
	clx := table[offset : offset+length]
	pos := 0
	for pos < len(clx) && clx[pos] == 0x01 {
		if pos+3 > len(clx) {
			return "", io.ErrUnexpectedEOF
		}
		size := int(binary.LittleEndian.Uint16(clx[pos+1 : pos+3]))
		pos += 3 + size
	}
	if pos+5 > len(clx) || clx[pos] != 0x02 {
		return "", errors.New("missing Pcdt")
	}
	plcSize := int(binary.LittleEndian.Uint32(clx[pos+1 : pos+5]))
	pos += 5
	if plcSize < 4 || pos+plcSize > len(clx) {
		return "", errors.New("invalid PlcPcd")
	}
	plc := clx[pos : pos+plcSize]
	pieces := (plcSize - 4) / 12
	if pieces <= 0 || pieces > 100000 {
		return "", errors.New("invalid piece count")
	}
	cpBytes := (pieces + 1) * 4
	if cpBytes+pieces*8 > len(plc) {
		return "", io.ErrUnexpectedEOF
	}
	var output strings.Builder
	for i := 0; i < pieces; i++ {
		cpStart := int(binary.LittleEndian.Uint32(plc[i*4 : i*4+4]))
		cpEnd := int(binary.LittleEndian.Uint32(plc[(i+1)*4 : (i+1)*4+4]))
		if cpEnd <= cpStart {
			continue
		}
		pcd := cpBytes + i*8
		rawFC := binary.LittleEndian.Uint32(plc[pcd+2 : pcd+6])
		compressed := rawFC&0x40000000 != 0
		fc := int(rawFC & 0x3FFFFFFF)
		chars := cpEnd - cpStart
		if compressed {
			fc /= 2
			if fc < 0 || fc+chars > len(word) {
				continue
			}
			decoded, _ := io.ReadAll(transform.NewReader(bytes.NewReader(word[fc:fc+chars]), simplifiedchinese.GB18030.NewDecoder()))
			output.Write(decoded)
		} else {
			byteLen := chars * 2
			if fc < 0 || fc+byteLen > len(word) {
				continue
			}
			units := make([]uint16, chars)
			for j := 0; j < chars; j++ {
				units[j] = binary.LittleEndian.Uint16(word[fc+j*2 : fc+j*2+2])
			}
			output.WriteString(string(utf16.Decode(units)))
		}
	}
	return output.String(), nil
}

func splitDramaChapters(content string) []DramaChapter {
	matches := chapterTitlePattern.FindAllStringSubmatchIndex(content, -1)
	if len(matches) == 0 {
		return nil
	}
	chapters := make([]DramaChapter, 0, len(matches))
	for i, match := range matches {
		start := match[0]
		if i == 0 {
			start = 0
		}
		end := len(content)
		if i+1 < len(matches) {
			end = matches[i+1][0]
		}
		body := strings.TrimSpace(content[start:end])
		if body == "" {
			continue
		}
		title := strings.TrimSpace(content[match[2]:match[3]])
		chapters = append(chapters, DramaChapter{Title: title, Content: body, CharCount: len([]rune(body))})
	}
	return chapters
}

func normalizeDramaText(value string) string {
	value = strings.ReplaceAll(value, "\x00", "")
	value = strings.ReplaceAll(strings.ReplaceAll(value, "\r\n", "\n"), "\r", "\n")
	lines := strings.Split(value, "\n")
	cleaned := make([]string, 0, len(lines))
	blank := false
	for _, line := range lines {
		line = strings.TrimRightFunc(line, unicode.IsSpace)
		if strings.TrimSpace(line) == "" {
			if !blank {
				cleaned = append(cleaned, "")
				blank = true
			}
			continue
		}
		cleaned = append(cleaned, line)
		blank = false
	}
	return strings.TrimSpace(strings.Join(cleaned, "\n"))
}

func looksLikeBinary(data []byte) bool {
	if len(data) == 0 {
		return false
	}
	controls := 0
	for _, b := range data {
		if b < 0x09 || (b > 0x0D && b < 0x20) {
			controls++
		}
	}
	return controls*100/len(data) > 2
}
func looksLikeHTMLOrScript(data []byte) bool {
	sample := strings.ToLower(string(data))
	markers := []string{"<!doctype html", "<html", "<head", "<body", "<script", "</script", "<iframe", "<object", "<embed", "javascript:", "vbscript:", "onerror=", "onload="}
	for _, marker := range markers {
		if strings.Contains(sample, marker) {
			return true
		}
	}
	return false
}
func hasDisallowedBinarySignature(data []byte) bool {
	signatures := [][]byte{
		{0x25, 0x50, 0x44, 0x46, 0x2D},
		{0x50, 0x4B, 0x03, 0x04},
		{0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1},
		{0x89, 0x50, 0x4E, 0x47},
		{0xFF, 0xD8, 0xFF},
		{0x47, 0x49, 0x46, 0x38},
		{0x4D, 0x5A},
		{0x7F, 0x45, 0x4C, 0x46},
		{0x52, 0x61, 0x72, 0x21},
		{0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C},
		{0x1F, 0x8B},
	}
	data = bytes.TrimSpace(data)
	for _, signature := range signatures {
		if bytes.HasPrefix(data, signature) {
			return true
		}
	}
	return false
}
func cleanDramaFilename(value string) string {
	value = filepath.Base(strings.TrimSpace(value))
	return truncateRunes(value, 255)
}
func truncateRunes(value string, limit int) string {
	runes := []rune(value)
	if len(runes) > limit {
		return string(runes[:limit])
	}
	return value
}

// Stable chapter ordering is useful in tests and when import previews are reconstructed.
func sortEpisodesByNumber(items []model.ProjectEpisode) {
	sort.SliceStable(items, func(i, j int) bool { return items[i].EpisodeNo < items[j].EpisodeNo })
}
