package synclegacy

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"

	_ "modernc.org/sqlite"

	"ymhut-box/server/unified-management/internal/config"
	"ymhut-box/server/unified-management/internal/db"
	"ymhut-box/server/unified-management/internal/notices"
)

type Service struct {
	cfg     *config.Config
	store   *db.Store
	notices *notices.Service
}

type Result struct {
	Ok       bool           `json:"ok"`
	DryRun   bool           `json:"dryRun"`
	Paths    map[string]any `json:"paths"`
	Stats    map[string]int `json:"stats"`
	Warnings []string       `json:"warnings"`
	Errors   []string       `json:"errors"`
	Started  string         `json:"startedAt"`
	Finished string         `json:"finishedAt"`
}

func New(cfg *config.Config, store *db.Store, noticeService *notices.Service) *Service {
	return &Service{cfg: cfg, store: store, notices: noticeService}
}

func (s *Service) Preview(ctx context.Context) Result {
	return s.run(ctx, true)
}

func (s *Service) Run(ctx context.Context) Result {
	return s.run(ctx, false)
}

func (s *Service) run(ctx context.Context, dryRun bool) Result {
	result := Result{
		Ok:     true,
		DryRun: dryRun,
		Paths: map[string]any{
			"legacyUpdateDir":       s.displayPath(s.cfg.LegacyUpdateDir),
			"legacyFeedbackDir":     s.displayPath(s.cfg.LegacyFeedbackDir),
			"legacyUpdateNoticeDir": s.displayPath(s.cfg.LegacyUpdateNoticeDir),
			"updatePublicDir":       s.displayPath(s.cfg.UpdatePublicDir),
			"updateNoticeDir":       s.displayPath(s.cfg.UpdateNoticeDir),
		},
		Stats:   map[string]int{},
		Started: db.Now(),
	}
	defer func() {
		result.Finished = db.Now()
		if len(result.Errors) > 0 {
			result.Ok = false
		}
	}()
	s.previewPath(&result, "legacy_update", s.cfg.LegacyUpdateDir)
	s.previewPath(&result, "legacy_feedback", s.cfg.LegacyFeedbackDir)
	s.previewPath(&result, "legacy_update_notice", s.cfg.LegacyUpdateNoticeDir)
	if dryRun {
		return result
	}
	if err := s.backupCurrent(); err != nil {
		result.Errors = append(result.Errors, err.Error())
		return result
	}
	s.syncUpdatePublic(&result)
	s.syncNotices(ctx, &result)
	s.syncFeedbackSQLite(&result)
	_ = s.store.InsertAudit(db.AuditLog{Actor: "admin", Type: "legacy.sync", Target: "legacy-projects", Message: fmt.Sprintf("旧项目同步完成：成功=%v，复制文件=%d，导入记录=%d，错误=%d", result.Ok, result.Stats["copiedFiles"], result.Stats["importedRows"], len(result.Errors))})
	return result
}

func (s *Service) displayPath(path string) string {
	if strings.TrimSpace(path) == "" {
		return ""
	}
	rel, err := filepath.Rel(s.cfg.BaseDir, path)
	if err != nil || rel == "" {
		return path
	}
	return filepath.ToSlash(rel)
}

func (s *Service) previewPath(result *Result, key, path string) {
	info, err := os.Stat(path)
	if err != nil {
		result.Warnings = append(result.Warnings, key+": "+err.Error())
		result.Stats["missingPaths"]++
		return
	}
	if !info.IsDir() {
		result.Warnings = append(result.Warnings, key+": path is not a directory")
		result.Stats["missingPaths"]++
	}
}

func (s *Service) backupCurrent() error {
	backupRoot := filepath.Join(s.cfg.StorageDir, "backups", "legacy-sync-"+time.Now().UTC().Format("20060102-150405"))
	for _, path := range []string{s.cfg.UpdatePublicDir, s.cfg.UpdateNoticeDir} {
		if _, err := os.Stat(path); err == nil {
			target := filepath.Join(backupRoot, filepath.Base(path))
			if err := copyDir(path, target); err != nil {
				return err
			}
		}
	}
	return nil
}

func (s *Service) syncUpdatePublic(result *Result) {
	sourcePublic := filepath.Join(s.cfg.LegacyUpdateDir, "public")
	if _, err := os.Stat(sourcePublic); err != nil {
		result.Warnings = append(result.Warnings, "update public not found: "+err.Error())
		return
	}
	for _, name := range []string{"update-info.json", "media-types.json", "tool-status.json", "modules.json"} {
		source := filepath.Join(sourcePublic, name)
		if _, err := os.Stat(source); err == nil {
			if err := copyFile(source, filepath.Join(s.cfg.UpdatePublicDir, name)); err != nil {
				result.Errors = append(result.Errors, err.Error())
			} else {
				result.Stats["copiedFiles"]++
			}
		}
	}
	sourceDownloads := filepath.Join(sourcePublic, "downloads")
	if _, err := os.Stat(sourceDownloads); err == nil {
		if err := copyDir(sourceDownloads, s.cfg.DownloadsDir); err != nil {
			result.Errors = append(result.Errors, err.Error())
		} else {
			result.Stats["copiedDirectories"]++
		}
	}
}

func (s *Service) syncNotices(ctx context.Context, result *Result) {
	for _, source := range []string{filepath.Join(s.cfg.LegacyUpdateDir, "update-notice"), s.cfg.LegacyUpdateNoticeDir} {
		if _, err := os.Stat(source); err != nil {
			continue
		}
		if err := copyDir(source, s.cfg.UpdateNoticeDir); err != nil {
			result.Errors = append(result.Errors, err.Error())
			continue
		}
		result.Stats["copiedDirectories"]++
	}
	if s.notices != nil {
		if err := s.notices.Import(ctx); err != nil {
			result.Errors = append(result.Errors, "notice import: "+err.Error())
		} else {
			result.Stats["noticeImports"]++
		}
	}
}

func (s *Service) syncFeedbackSQLite(result *Result) {
	path := filepath.Join(s.cfg.LegacyFeedbackDir, "storage", "feedback.sqlite")
	if _, err := os.Stat(path); err != nil {
		result.Warnings = append(result.Warnings, "feedback sqlite not found: "+err.Error())
		return
	}
	oldDB, err := sql.Open("sqlite", path)
	if err != nil {
		result.Errors = append(result.Errors, err.Error())
		return
	}
	defer oldDB.Close()
	s.importOldFeedbacks(oldDB, result)
	s.importOldComments(oldDB, result)
	s.importOldEvents(oldDB, result)
	s.importOldTags(oldDB, result)
	s.importOldMail(oldDB, result)
	s.importOldWebhooks(oldDB, result)
}

func (s *Service) importOldFeedbacks(oldDB *sql.DB, result *Result) {
	rows, err := oldDB.Query(`SELECT code, received_at, title, type, severity, category, priority, contact, body, status, status_detail, note, public_reply, handled_by, assignee, due_at, resolved_at, archived_at, sla_level, source_channel, risk_score, resolution, package_path, encrypted_package_path, package_sha256, plain_package_sha256, remote_addr, summary_text, included_files, mail_sent, updated_at, last_activity_at FROM feedbacks`)
	if err != nil {
		result.Warnings = append(result.Warnings, "feedbacks: "+err.Error())
		return
	}
	defer rows.Close()
	for rows.Next() {
		var item db.Feedback
		var mailSent int
		if err := rows.Scan(&item.Code, &item.CreatedAt, &item.Title, &item.Type, &item.Severity, &item.Category, &item.Priority, &item.Contact, &item.Body, &item.Status, &item.StatusDetail, &item.Note, &item.PublicReply, &item.HandledBy, &item.Assignee, &item.DueAt, &item.ResolvedAt, &item.ArchivedAt, &item.SLALevel, &item.SourceChannel, &item.RiskScore, &item.Resolution, &item.PackagePath, &item.EncryptedPackagePath, &item.PackageSha256, &item.PlainPackageSha256, &item.RemoteAddr, &item.SummaryText, &item.IncludedFiles, &mailSent, &item.UpdatedAt, &item.LastActivityAt); err != nil {
			result.Errors = append(result.Errors, "feedback scan: "+err.Error())
			continue
		}
		item.MailSent = mailSent == 1
		item.PackagePath = s.copyLegacyFeedbackFile(item.PackagePath, item.Code, result)
		item.EncryptedPackagePath = s.copyLegacyFeedbackFile(item.EncryptedPackagePath, item.Code, result)
		if err := s.store.InsertFeedback(item); err != nil && !strings.Contains(strings.ToLower(err.Error()), "unique") {
			result.Errors = append(result.Errors, "feedback import "+item.Code+": "+err.Error())
		} else {
			result.Stats["importedRows"]++
		}
	}
}

func (s *Service) importOldComments(oldDB *sql.DB, result *Result) {
	rows, err := oldDB.Query(`SELECT id, feedback_code, author, body, internal, created_at FROM feedback_comments`)
	if err != nil {
		result.Warnings = append(result.Warnings, "comments: "+err.Error())
		return
	}
	defer rows.Close()
	for rows.Next() {
		var comment db.FeedbackComment
		var internal int
		var oldID int64
		if err := rows.Scan(&oldID, &comment.Code, &comment.Author, &comment.Body, &internal, &comment.CreatedAt); err != nil {
			result.Errors = append(result.Errors, "comment scan: "+err.Error())
			continue
		}
		comment.Internal = internal == 1
		if _, err := s.store.InsertFeedbackComment(comment); err == nil {
			result.Stats["importedRows"]++
		}
	}
}

func (s *Service) importOldEvents(oldDB *sql.DB, result *Result) {
	rows, err := oldDB.Query(`SELECT id, feedback_code, event_type, actor, from_value, to_value, message, created_at FROM feedback_events`)
	if err != nil {
		result.Warnings = append(result.Warnings, "events: "+err.Error())
		return
	}
	defer rows.Close()
	for rows.Next() {
		var event db.LegacyFeedbackEvent
		if err := rows.Scan(&event.ID, &event.FeedbackCode, &event.EventType, &event.Actor, &event.FromValue, &event.ToValue, &event.Message, &event.CreatedAt); err != nil {
			result.Errors = append(result.Errors, "event scan: "+err.Error())
			continue
		}
		if err := s.store.UpsertFeedbackEvent(event); err == nil {
			result.Stats["importedRows"]++
		}
	}
}

func (s *Service) importOldTags(oldDB *sql.DB, result *Result) {
	rows, err := oldDB.Query(`SELECT feedback_code, tag, created_at FROM feedback_tags`)
	if err != nil {
		result.Warnings = append(result.Warnings, "tags: "+err.Error())
		return
	}
	defer rows.Close()
	for rows.Next() {
		var code, tag, createdAt string
		if err := rows.Scan(&code, &tag, &createdAt); err != nil {
			continue
		}
		if err := s.store.UpsertFeedbackTag(code, tag, createdAt); err == nil {
			result.Stats["importedRows"]++
		}
	}
}

func (s *Service) importOldMail(oldDB *sql.DB, result *Result) {
	rows, err := oldDB.Query(`SELECT id, feedback_code, kind, status, to_address, subject, attachment_path, attachment_name, error_message, created_at, sent_at FROM mail_records`)
	if err != nil {
		result.Warnings = append(result.Warnings, "mail_records: "+err.Error())
		return
	}
	defer rows.Close()
	for rows.Next() {
		var item db.LegacyMailRecord
		if err := rows.Scan(&item.ID, &item.FeedbackCode, &item.Kind, &item.Status, &item.ToAddress, &item.Subject, &item.AttachmentPath, &item.AttachmentName, &item.ErrorMessage, &item.CreatedAt, &item.SentAt); err != nil {
			result.Errors = append(result.Errors, "mail scan: "+err.Error())
			continue
		}
		item.AttachmentPath = s.copyLegacyFeedbackFile(item.AttachmentPath, item.FeedbackCode, result)
		if err := s.store.UpsertMailRecord(item); err == nil {
			result.Stats["importedRows"]++
		}
	}
}

func (s *Service) importOldWebhooks(oldDB *sql.DB, result *Result) {
	rows, err := oldDB.Query(`SELECT id, webhook_name, event, status, attempts, response_code, error_message, payload_sha256, created_at, finished_at FROM webhook_deliveries`)
	if err != nil {
		result.Warnings = append(result.Warnings, "webhook_deliveries: "+err.Error())
		return
	}
	defer rows.Close()
	for rows.Next() {
		var id int64
		var name, event, status, message, payload, createdAt, finishedAt string
		var attempts, response int
		if err := rows.Scan(&id, &name, &event, &status, &attempts, &response, &message, &payload, &createdAt, &finishedAt); err != nil {
			continue
		}
		_ = s.store.InsertAudit(db.AuditLog{Actor: "legacy", Type: "webhook." + status, Target: name, Message: "旧反馈 Webhook 记录：" + strings.TrimSpace(event+" "+message), CreatedAt: firstNonEmpty(createdAt, finishedAt, db.Now())})
		result.Stats["importedRows"]++
	}
}

func (s *Service) copyLegacyFeedbackFile(path, code string, result *Result) string {
	if strings.TrimSpace(path) == "" {
		return ""
	}
	source := path
	if !filepath.IsAbs(source) {
		source = filepath.Join(s.cfg.LegacyFeedbackDir, source)
	}
	info, err := os.Stat(source)
	if err != nil || info.IsDir() {
		return path
	}
	target := filepath.Join(s.cfg.StorageDir, "legacy-feedback", safeName(code), filepath.Base(source))
	if err := copyFile(source, target); err != nil {
		result.Warnings = append(result.Warnings, "copy attachment: "+err.Error())
		return path
	}
	result.Stats["copiedFiles"]++
	return target
}

func copyDir(source, target string) error {
	sourceInfo, err := os.Stat(source)
	if err != nil {
		return err
	}
	if !sourceInfo.IsDir() {
		return errors.New(source + " is not a directory")
	}
	return filepath.WalkDir(source, func(path string, entry os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(source, path)
		if err != nil {
			return err
		}
		dest := filepath.Join(target, rel)
		if entry.IsDir() {
			return os.MkdirAll(dest, 0o750)
		}
		return copyFile(path, dest)
	})
}

func copyFile(source, target string) error {
	if err := os.MkdirAll(filepath.Dir(target), 0o750); err != nil {
		return err
	}
	in, err := os.Open(source)
	if err != nil {
		return err
	}
	defer in.Close()
	out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o640)
	if err != nil {
		return err
	}
	defer out.Close()
	_, err = io.Copy(out, in)
	return err
}

func safeName(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return "unknown"
	}
	return strings.Map(func(r rune) rune {
		if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' {
			return r
		}
		return '-'
	}, value)
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}
