package notices

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

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

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

type Document struct {
	Notice    db.ReleaseNotice           `json:"notice"`
	Raw       string                     `json:"raw"`
	Parsed    map[string]any             `json:"parsed"`
	Path      string                     `json:"path"`
	UpdatedAt string                     `json:"updatedAt"`
	Revisions []db.ReleaseNoticeRevision `json:"revisions"`
}

type SaveRequest struct {
	Raw    string         `json:"raw"`
	Parsed map[string]any `json:"parsed"`
	Note   string         `json:"note"`
}

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

func (s *Service) Import(ctx context.Context) error {
	if strings.TrimSpace(s.cfg.UpdateNoticeDir) == "" {
		return nil
	}
	if _, err := os.Stat(s.cfg.UpdateNoticeDir); errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err := s.importTotalIndex(); err != nil {
		return err
	}
	entries, err := os.ReadDir(s.cfg.UpdateNoticeDir)
	if err != nil {
		return err
	}
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".json") || strings.EqualFold(entry.Name(), "total.json") {
			continue
		}
		path := filepath.Join(s.cfg.UpdateNoticeDir, entry.Name())
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		item, _, _, err := parseNotice(data, versionFromFile(entry.Name()), entry.Name())
		if err != nil {
			return err
		}
		if _, err := s.store.UpsertReleaseNotice(item); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) List(limit int) ([]db.ReleaseNotice, error) {
	return s.store.ListReleaseNotices(limit)
}

func (s *Service) Get(version string) (Document, error) {
	item, err := s.store.GetReleaseNotice(version)
	if err != nil {
		return Document{}, err
	}
	raw := item.RawJSON
	parsed, formatted, err := parseAndFormat([]byte(raw), version, item.NoticeFile)
	if err != nil {
		return Document{}, err
	}
	revisions, _ := s.store.ListReleaseNoticeRevisions(version, 20)
	updatedAt := item.UpdatedAt
	path := filepath.Join(s.cfg.UpdateNoticeDir, firstNonEmpty(item.NoticeFile, version+".json"))
	if info, err := os.Stat(path); err == nil {
		updatedAt = info.ModTime().UTC().Format(time.RFC3339)
	}
	return Document{Notice: item, Raw: formatted, Parsed: parsed, Path: path, UpdatedAt: updatedAt, Revisions: revisions}, nil
}

func (s *Service) Validate(ctx context.Context, version string, req SaveRequest) (Document, error) {
	raw, err := requestRaw(req)
	if err != nil {
		return Document{}, err
	}
	item, parsed, formatted, err := parseNotice([]byte(raw), version, version+".json")
	if err != nil {
		return Document{}, err
	}
	return Document{Notice: item, Raw: formatted, Parsed: parsed}, nil
}

func (s *Service) Save(ctx context.Context, version string, req SaveRequest, actor string) (Document, error) {
	raw, err := requestRaw(req)
	if err != nil {
		return Document{}, err
	}
	item, parsed, formatted, err := parseNotice([]byte(raw), version, version+".json")
	if err != nil {
		return Document{}, err
	}
	item.RawJSON = formatted
	if current, err := s.store.GetReleaseNotice(item.Version); err == nil && current.RawJSON != "" {
		_, _ = s.store.SaveReleaseNoticeRevision(item.Version, current.RawJSON, "auto backup before save", actor)
	}
	saved, err := s.store.UpsertReleaseNotice(item)
	if err != nil {
		return Document{}, err
	}
	_, _ = s.store.SaveReleaseNoticeRevision(saved.Version, formatted, req.Note, actor)
	if err := s.writeNoticeFile(saved, formatted); err != nil {
		return Document{}, err
	}
	if err := s.writeTotalIndex(saved, parsed); err != nil {
		return Document{}, err
	}
	if err := s.syncLegacyUpdateInfo(saved, parsed); err != nil {
		return Document{}, err
	}
	_ = s.store.InsertAudit(db.AuditLog{Actor: actor, Type: "release_notice.saved", Target: saved.Version, Message: "版本日志已保存并同步兼容更新信息"})
	return s.Get(saved.Version)
}

func (s *Service) SyncFromLegacyUpdateInfo(ctx context.Context, raw string, actor string) error {
	if strings.TrimSpace(raw) == "" {
		return nil
	}
	item, parsed, formatted, err := parseNotice([]byte(raw), "", "")
	if err != nil {
		return err
	}
	item.RawJSON = formatted
	current, err := s.store.GetReleaseNotice(item.Version)
	if err == nil && current.RawJSON != "" && current.RawJSON != formatted {
		_, _ = s.store.SaveReleaseNoticeRevision(item.Version, current.RawJSON, "auto backup before legacy update-info sync", actor)
	}
	saved, err := s.store.UpsertReleaseNotice(item)
	if err != nil {
		return err
	}
	_, _ = s.store.SaveReleaseNoticeRevision(saved.Version, formatted, "synced from update-info.json", actor)
	if err := s.writeNoticeFile(saved, formatted); err != nil {
		return err
	}
	if err := s.writeTotalIndex(saved, parsed); err != nil {
		return err
	}
	_ = s.store.InsertAudit(db.AuditLog{Actor: actor, Type: "release_notice.synced", Target: saved.Version, Message: "版本日志已从兼容 update-info.json 同步"})
	return nil
}

func (s *Service) Restore(ctx context.Context, version string, revisionID int64, actor string) (Document, error) {
	revision, err := s.store.GetReleaseNoticeRevision(version, revisionID)
	if err != nil {
		return Document{}, err
	}
	return s.Save(ctx, version, SaveRequest{Raw: revision.RawJSON, Note: "restored revision"}, actor)
}

func (s *Service) importTotalIndex() error {
	path := filepath.Join(s.cfg.UpdateNoticeDir, "total.json")
	data, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}
	var root map[string]any
	if err := json.Unmarshal(data, &root); err != nil {
		return err
	}
	if latest, ok := root["latest"].(map[string]any); ok {
		raw, _ := json.MarshalIndent(latest, "", "  ")
		item, _, _, err := parseNotice(raw, stringValue(root, "latest_version"), stringValue(root, "latest_notice_file"))
		if err == nil {
			_, _ = s.store.UpsertReleaseNotice(item)
		}
	}
	for _, entry := range arrayValue(root, "versions") {
		version := stringValue(entry, "version")
		if version == "" {
			continue
		}
		raw, _ := json.MarshalIndent(entry, "", "  ")
		item, _, _, err := parseNotice(raw, version, stringValue(entry, "notice_file"))
		if err == nil {
			_, _ = s.store.UpsertReleaseNotice(item)
		}
	}
	return nil
}

func (s *Service) writeNoticeFile(item db.ReleaseNotice, raw string) error {
	if err := os.MkdirAll(s.cfg.UpdateNoticeDir, 0o750); err != nil {
		return err
	}
	return atomicWrite(filepath.Join(s.cfg.UpdateNoticeDir, firstNonEmpty(item.NoticeFile, item.Version+".json")), []byte(raw))
}

func (s *Service) writeTotalIndex(item db.ReleaseNotice, parsed map[string]any) error {
	path := filepath.Join(s.cfg.UpdateNoticeDir, "total.json")
	root := map[string]any{"schema_version": 1, "product": "YMhut Box", "versions": []any{}}
	if data, err := os.ReadFile(path); err == nil {
		_ = json.Unmarshal(data, &root)
	}
	root["latest_version"] = newestVersion(stringValue(root, "latest_version"), item.Version)
	if root["latest_version"] == item.Version {
		root["latest_notice_file"] = item.NoticeFile
		root["latest"] = latestMap(item, parsed)
	}
	root["last_updated"] = db.Now()
	versions := arrayValue(root, "versions")
	next := make([]any, 0, len(versions)+1)
	found := false
	for _, entry := range versions {
		if stringValue(entry, "version") == item.Version {
			next = append(next, summaryMap(item, parsed))
			found = true
			continue
		}
		next = append(next, entry)
	}
	if !found {
		next = append(next, summaryMap(item, parsed))
	}
	sort.SliceStable(next, func(i, j int) bool {
		return compareVersion(stringValue(next[i], "version"), stringValue(next[j], "version")) > 0
	})
	root["versions"] = next
	data, err := json.MarshalIndent(root, "", "  ")
	if err != nil {
		return err
	}
	return atomicWrite(path, append(data, '\n'))
}

func (s *Service) syncLegacyUpdateInfo(item db.ReleaseNotice, parsed map[string]any) error {
	path := filepath.Join(s.cfg.UpdatePublicDir, "update-info.json")
	payload := s.legacyUpdateBase(path)
	payload["app_version"] = item.Version
	setNonEmpty(payload, "build", item.Build)
	setNonEmpty(payload, "channel", item.Channel)
	setNonEmpty(payload, "title", item.Title)
	setNonEmpty(payload, "message", item.Message)
	setNonEmpty(payload, "message_md", item.MessageMD)
	setNonEmpty(payload, "release_notes", item.ReleaseNotes)
	setNonEmpty(payload, "release_notes_md", item.ReleaseNotesMD)
	setNonEmpty(payload, "download_url", item.DownloadURL)
	payload["last_updated"] = firstNonEmpty(item.PublishedAt, db.Now())
	for _, key := range []string{"update_notes", "last_update_notes", "download_mirrors", "detected_packages", "detected_product", "category_list", "home_notes", "tool_metadata", "api_keys"} {
		if value, ok := parsed[key]; ok {
			payload[key] = value
		}
	}
	data, err := json.MarshalIndent(payload, "", "  ")
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
		return err
	}
	return atomicWrite(path, append(data, '\n'))
}

func (s *Service) legacyUpdateBase(currentPath string) map[string]any {
	payload := map[string]any{}
	for _, path := range []string{
		filepath.Join(s.cfg.LegacyUpdateDir, "public", "update-info.json"),
		currentPath,
	} {
		if data, err := os.ReadFile(path); err == nil {
			var doc map[string]any
			if json.Unmarshal(data, &doc) == nil {
				for key, value := range doc {
					payload[key] = value
				}
			}
		}
	}
	if payload["app_version"] == nil {
		if value, ok := payload["appVersion"]; ok {
			payload["app_version"] = value
		} else if value, ok := payload["latestVersion"]; ok {
			payload["app_version"] = value
		}
	}
	if payload["manifest_version"] == nil {
		if value, ok := payload["manifestVersion"]; ok {
			payload["manifest_version"] = value
		}
	}
	return payload
}

func parseAndFormat(data []byte, fallbackVersion, noticeFile string) (map[string]any, string, error) {
	_, parsed, formatted, err := parseNotice(data, fallbackVersion, noticeFile)
	return parsed, formatted, err
}

func parseNotice(data []byte, fallbackVersion, noticeFile string) (db.ReleaseNotice, map[string]any, string, error) {
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.UseNumber()
	var parsed map[string]any
	if err := decoder.Decode(&parsed); err != nil {
		return db.ReleaseNotice{}, nil, "", err
	}
	version := firstNonEmpty(stringValue(parsed, "app_version"), stringValue(parsed, "version"), fallbackVersion)
	if version == "" {
		return db.ReleaseNotice{}, nil, "", errors.New("版本日志需要填写 version 或 app_version")
	}
	if noticeFile == "" {
		noticeFile = version + ".json"
	}
	formattedBytes, err := json.MarshalIndent(parsed, "", "  ")
	if err != nil {
		return db.ReleaseNotice{}, nil, "", err
	}
	formatted := string(formattedBytes) + "\n"
	item := db.ReleaseNotice{
		Version:        version,
		Build:          stringValue(parsed, "build"),
		Channel:        firstNonEmpty(stringValue(parsed, "channel"), "stable"),
		Title:          firstNonEmpty(stringValue(parsed, "title"), "YMhut Box "+version),
		Message:        firstNonEmpty(stringValue(parsed, "message"), stringValue(parsed, "summary"), stringValue(parsed, "home_notes")),
		ReleaseNotes:   firstNonEmpty(stringValue(parsed, "release_notes"), stringValue(parsed, "summary")),
		MessageMD:      stringValue(parsed, "message_md"),
		ReleaseNotesMD: stringValue(parsed, "release_notes_md"),
		DownloadURL:    stringValue(parsed, "download_url"),
		NoticeFile:     noticeFile,
		RawJSON:        formatted,
		PublishedAt:    normalizeTime(firstNonEmpty(stringValue(parsed, "published_at"), stringValue(parsed, "release_date"), stringValue(parsed, "last_updated"))),
	}
	return item, parsed, formatted, nil
}

func requestRaw(req SaveRequest) (string, error) {
	if strings.TrimSpace(req.Raw) != "" {
		return req.Raw, nil
	}
	if req.Parsed == nil {
		return "", errors.New("raw or parsed JSON is required")
	}
	data, err := json.Marshal(req.Parsed)
	return string(data), err
}

func latestMap(item db.ReleaseNotice, parsed map[string]any) map[string]any {
	out := summaryMap(item, parsed)
	out["title"] = item.Title
	out["message"] = item.Message
	out["download_url"] = item.DownloadURL
	out["release_notes"] = item.ReleaseNotes
	out["message_md"] = item.MessageMD
	out["release_notes_md"] = item.ReleaseNotesMD
	return out
}

func summaryMap(item db.ReleaseNotice, parsed map[string]any) map[string]any {
	out := map[string]any{
		"version":      item.Version,
		"build":        item.Build,
		"channel":      item.Channel,
		"release_date": dateOnly(item.PublishedAt),
		"notice_file":  item.NoticeFile,
		"summary":      firstNonEmpty(item.Message, item.ReleaseNotes),
	}
	if value, ok := parsed["highlights"]; ok {
		out["highlights"] = value
	}
	if value, ok := parsed["categories"]; ok {
		out["categories"] = value
	} else if value, ok := parsed["update_notes"]; ok {
		out["categories"] = value
	}
	return out
}

func PublicNotice(item db.ReleaseNotice) map[string]any {
	return map[string]any{
		"version":              item.Version,
		"build":                item.Build,
		"channel":              item.Channel,
		"title":                item.Title,
		"message":              item.Message,
		"release_notes":        item.ReleaseNotes,
		"message_md":           item.MessageMD,
		"release_notes_md":     item.ReleaseNotesMD,
		"download_url":         item.DownloadURL,
		"notice_file":          item.NoticeFile,
		"published_at":         item.PublishedAt,
		"updated_at":           item.UpdatedAt,
		"releaseNotes":         item.ReleaseNotes,
		"messageMarkdown":      item.MessageMD,
		"releaseNotesMarkdown": item.ReleaseNotesMD,
	}
}

func PublicList(items []db.ReleaseNotice) []map[string]any {
	out := make([]map[string]any, 0, len(items))
	for _, item := range items {
		out = append(out, PublicNotice(item))
	}
	return out
}

func arrayValue(root map[string]any, key string) []any {
	if values, ok := root[key].([]any); ok {
		return values
	}
	return []any{}
}

func stringValue(root any, key string) string {
	obj, ok := root.(map[string]any)
	if !ok {
		return ""
	}
	switch value := obj[key].(type) {
	case string:
		return strings.TrimSpace(value)
	case json.Number:
		return value.String()
	default:
		return ""
	}
}

func setNonEmpty(target map[string]any, key, value string) {
	if strings.TrimSpace(value) != "" {
		target[key] = value
	}
}

func normalizeTime(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return ""
	}
	if t, err := time.Parse(time.RFC3339, value); err == nil {
		return t.UTC().Format(time.RFC3339)
	}
	if t, err := time.Parse("2006-01-02", value); err == nil {
		return t.UTC().Format(time.RFC3339)
	}
	return value
}

func dateOnly(value string) string {
	if t, err := time.Parse(time.RFC3339, value); err == nil {
		return t.UTC().Format("2006-01-02")
	}
	if len(value) >= 10 {
		return value[:10]
	}
	return value
}

func newestVersion(current, candidate string) string {
	if current == "" || compareVersion(candidate, current) >= 0 {
		return candidate
	}
	return current
}

func compareVersion(a, b string) int {
	as := strings.Split(a, ".")
	bs := strings.Split(b, ".")
	for len(as) < 4 {
		as = append(as, "0")
	}
	for len(bs) < 4 {
		bs = append(bs, "0")
	}
	for i := 0; i < 4; i++ {
		ai := parseInt(as[i])
		bi := parseInt(bs[i])
		if ai > bi {
			return 1
		}
		if ai < bi {
			return -1
		}
	}
	return 0
}

func parseInt(value string) int {
	n := 0
	for _, r := range value {
		if r < '0' || r > '9' {
			break
		}
		n = n*10 + int(r-'0')
	}
	return n
}

func versionFromFile(name string) string {
	return strings.TrimSuffix(filepath.Base(name), filepath.Ext(name))
}

func atomicWrite(path string, data []byte) error {
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0o750); err != nil {
		return err
	}
	tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp")
	if err != nil {
		return err
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)
	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	if err := os.Chmod(tmpName, 0o640); err != nil {
		return err
	}
	return os.Rename(tmpName, path)
}

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