package releases

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"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 Package struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Version   string `json:"version"`
	Platform  string `json:"platform"`
	Arch      string `json:"arch"`
	URL       string `json:"url"`
	SHA256    string `json:"sha256"`
	Size      int64  `json:"size"`
	Required  bool   `json:"required"`
	Enabled   bool   `json:"enabled"`
	FileName  string `json:"fileName"`
	UpdatedAt string `json:"updatedAt"`
}

type UploadOptions struct {
	FileName       string
	Version        string
	Platform       string
	Arch           string
	Channel        string
	Notes          string
	UpdateManifest bool
}

func NewService(cfg *config.Config, store *db.Store, noticeService ...*notices.Service) *Service {
	service := &Service{cfg: cfg, store: store}
	if len(noticeService) > 0 {
		service.notices = noticeService[0]
	}
	return service
}

func (s *Service) LegacyUpdateInfo(r *http.Request) map[string]any {
	payload := s.legacyUpdateBase()
	manifest := s.Manifest(r)
	for _, key := range []string{"app_version", "download_url", "download_mirrors", "detected_product", "detected_packages", "packages", "modules", "manifest_version", "release_notes", "release_notes_md", "message", "message_md", "notices", "latest_notice"} {
		if value, ok := manifest[key]; ok {
			payload[key] = value
		}
	}
	return payload
}

func (s *Service) Manifest(r *http.Request) map[string]any {
	payload := s.legacyUpdateBase()
	packages := s.ScanPackages(r)
	modules := readJSON(filepath.Join(s.cfg.UpdatePublicDir, "modules.json"))["modules"]
	if modules == nil {
		modules = []any{}
	}
	payload["manifest_version"] = 2
	payload["service_version"] = config.Version
	payload["packages"] = packages
	payload["modules"] = modules
	payload["assets"] = []any{}
	payload["generated_at"] = time.Now().UTC().Format(time.RFC3339)
	if s.notices != nil {
		if items, err := s.notices.List(50); err == nil && len(items) > 0 {
			publicNotices := notices.PublicList(items)
			payload["notices"] = publicNotices
			payload["latest_notice"] = publicNotices[0]
			latestNotice := items[0]
			setIfMissing(payload, "app_version", latestNotice.Version)
			setIfMissing(payload, "title", latestNotice.Title)
			setIfMissing(payload, "message", latestNotice.Message)
			setIfMissing(payload, "message_md", latestNotice.MessageMD)
			setIfMissing(payload, "release_notes", latestNotice.ReleaseNotes)
			setIfMissing(payload, "release_notes_md", latestNotice.ReleaseNotesMD)
			setIfMissing(payload, "download_url", latestNotice.DownloadURL)
		}
	}
	if len(packages) > 0 {
		latest := packages[0]
		payload["app_version"] = latest.Version
		payload["download_url"] = latest.URL
		payload["download_mirrors"] = []map[string]any{{
			"id":      "primary",
			"name":    "官方直连",
			"url":     latest.URL,
			"type":    "direct",
			"sha256":  latest.SHA256,
			"enabled": true,
		}}
		payload["detected_product"] = latest.Name
	}
	return payload
}

func (s *Service) PublishLegacyUpdateInfo(r *http.Request, actor string) error {
	payload := s.LegacyUpdateInfo(r)
	data, err := json.MarshalIndent(payload, "", "  ")
	if err != nil {
		return err
	}
	path := filepath.Join(s.cfg.UpdatePublicDir, "update-info.json")
	if err := atomicWrite(path, append(data, '\n')); err != nil {
		return err
	}
	_, _ = s.store.SaveLegacyRevision("update-info", string(append(data, '\n')), "generated from release database", firstNonEmpty(actor, "system"))
	return nil
}

func setIfMissing(payload map[string]any, key, value string) {
	if strings.TrimSpace(value) == "" {
		return
	}
	if existing, ok := payload[key].(string); !ok || strings.TrimSpace(existing) == "" {
		payload[key] = value
	}
}

func (s *Service) ScanPackages(r *http.Request) []Package {
	entries, err := os.ReadDir(s.cfg.DownloadsDir)
	if err != nil {
		return []Package{}
	}
	base := requestBaseURL(r, s.cfg.BaseURL)
	items := []Package{}
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		name := entry.Name()
		lower := strings.ToLower(name)
		if !(strings.HasSuffix(lower, ".exe") || strings.HasSuffix(lower, ".msix") || strings.HasSuffix(lower, ".appinstaller") || strings.HasSuffix(lower, ".msi")) {
			continue
		}
		info, err := entry.Info()
		if err != nil {
			continue
		}
		version := detectVersion(name)
		platform, arch := detectPlatform(name)
		product := detectProduct(name)
		url := base + "/downloads/" + name
		items = append(items, Package{
			ID:        strings.ToLower(strings.ReplaceAll(product+"-"+platform+"-"+arch+"-"+version, " ", "-")),
			Name:      product,
			Version:   version,
			Platform:  platform,
			Arch:      arch,
			URL:       url,
			SHA256:    sha256File(filepath.Join(s.cfg.DownloadsDir, name)),
			Size:      info.Size(),
			Required:  strings.Contains(strings.ToLower(product), "ymhut"),
			Enabled:   true,
			FileName:  name,
			UpdatedAt: info.ModTime().UTC().Format(time.RFC3339),
		})
	}
	sort.Slice(items, func(i, j int) bool {
		return compareVersion(items[i].Version, items[j].Version) > 0
	})
	return items
}

func (s *Service) StaticJSON(name string) map[string]any {
	return readJSON(filepath.Join(s.cfg.UpdatePublicDir, name))
}

func (s *Service) SaveUploadedPackage(r *http.Request, reader io.Reader, opts UploadOptions, actor string) (Package, error) {
	name, err := safePackageName(opts.FileName)
	if err != nil {
		return Package{}, err
	}
	if err := os.MkdirAll(s.cfg.DownloadsDir, 0o750); err != nil {
		return Package{}, err
	}
	target := filepath.Join(s.cfg.DownloadsDir, name)
	resolved, err := filepath.Abs(target)
	if err != nil {
		return Package{}, err
	}
	base, _ := filepath.Abs(s.cfg.DownloadsDir)
	if resolved != base && !strings.HasPrefix(resolved, base+string(os.PathSeparator)) {
		return Package{}, errors.New("path escape rejected")
	}
	tmp, err := os.CreateTemp(s.cfg.DownloadsDir, "."+name+".*.upload")
	if err != nil {
		return Package{}, err
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)
	hash := sha256.New()
	written, err := io.Copy(tmp, io.TeeReader(reader, hash))
	if closeErr := tmp.Close(); err == nil {
		err = closeErr
	}
	if err != nil {
		return Package{}, err
	}
	if written <= 0 {
		return Package{}, errors.New("uploaded file is empty")
	}
	if err := os.Chmod(tmpName, 0o640); err != nil {
		return Package{}, err
	}
	if err := os.Rename(tmpName, target); err != nil {
		return Package{}, err
	}
	version := firstNonEmpty(opts.Version, detectVersion(name))
	platform, arch := detectPlatform(name)
	platform = firstNonEmpty(opts.Platform, platform)
	arch = firstNonEmpty(opts.Arch, arch)
	product := detectProduct(name)
	pkg := Package{
		ID:        strings.ToLower(strings.ReplaceAll(product+"-"+platform+"-"+arch+"-"+version, " ", "-")),
		Name:      product,
		Version:   version,
		Platform:  platform,
		Arch:      arch,
		URL:       requestBaseURL(r, s.cfg.BaseURL) + "/downloads/" + name,
		SHA256:    hex.EncodeToString(hash.Sum(nil)),
		Size:      written,
		Required:  strings.Contains(strings.ToLower(product), "ymhut"),
		Enabled:   true,
		FileName:  name,
		UpdatedAt: time.Now().UTC().Format(time.RFC3339),
	}
	if opts.UpdateManifest {
		if err := s.updateLegacyManifest(pkg, opts); err != nil {
			return Package{}, err
		}
	}
	_ = s.store.InsertAudit(db.AuditLog{Actor: firstNonEmpty(actor, "admin"), Type: "release.package_uploaded", Target: name, Message: fmt.Sprintf("已上传发布包 %s（%s，%s）", name, version, formatBytes(written))})
	return pkg, nil
}

func (s *Service) updateLegacyManifest(pkg Package, opts UploadOptions) error {
	path := filepath.Join(s.cfg.UpdatePublicDir, "update-info.json")
	payload := s.legacyUpdateBase()
	payload["app_version"] = pkg.Version
	payload["download_url"] = pkg.URL
	payload["package_sha256"] = pkg.SHA256
	payload["package_size"] = pkg.Size
	payload["updated_at"] = time.Now().UTC().Format(time.RFC3339)
	payload["download_mirrors"] = []map[string]any{{
		"id":      "primary",
		"name":    "官方直连",
		"url":     pkg.URL,
		"type":    "direct",
		"sha256":  pkg.SHA256,
		"enabled": true,
	}}
	if strings.TrimSpace(opts.Notes) != "" {
		payload["release_notes"] = opts.Notes
	}
	data, err := json.MarshalIndent(payload, "", "  ")
	if err != nil {
		return err
	}
	return atomicWrite(path, append(data, '\n'))
}

func (s *Service) legacyUpdateBase() map[string]any {
	payload := map[string]any{}
	for _, path := range []string{
		filepath.Join(s.cfg.LegacyUpdateDir, "public", "update-info.json"),
		filepath.Join(s.cfg.UpdatePublicDir, "update-info.json"),
	} {
		for key, value := range readJSON(path) {
			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 readJSON(path string) map[string]any {
	data, err := os.ReadFile(path)
	if err != nil {
		return map[string]any{}
	}
	var payload map[string]any
	if err := json.Unmarshal(data, &payload); err != nil {
		return map[string]any{}
	}
	return payload
}

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 requestBaseURL(r *http.Request, fallback string) string {
	if r != nil {
		scheme := r.Header.Get("X-Forwarded-Proto")
		if scheme == "" {
			if r.TLS != nil {
				scheme = "https"
			} else {
				scheme = "http"
			}
		}
		if r.Host != "" {
			return scheme + "://" + r.Host
		}
	}
	return strings.TrimRight(fallback, "/")
}

var versionPattern = regexp.MustCompile(`\d+\.\d+\.\d+(?:\.\d+)?`)

func detectVersion(name string) string {
	match := versionPattern.FindString(name)
	if match == "" {
		return "0.0.0"
	}
	return match
}

func detectPlatform(name string) (string, string) {
	lower := strings.ToLower(name)
	platform := "windows"
	if strings.Contains(lower, "appinstaller") || strings.HasSuffix(lower, ".msix") || strings.HasSuffix(lower, ".exe") || strings.HasSuffix(lower, ".msi") {
		platform = "windows"
	}
	arch := "x64"
	if strings.Contains(lower, "arm64") {
		arch = "arm64"
	} else if strings.Contains(lower, "x86") && !strings.Contains(lower, "x64") {
		arch = "x86"
	}
	return platform, arch
}

func detectProduct(name string) string {
	if strings.Contains(strings.ToLower(name), "ymhut") {
		return "YMhut Box"
	}
	return "YMhut Package"
}

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, _ := strconv.Atoi(as[i])
		bi, _ := strconv.Atoi(bs[i])
		if ai > bi {
			return 1
		}
		if ai < bi {
			return -1
		}
	}
	return 0
}

func sha256File(path string) string {
	file, err := os.Open(path)
	if err != nil {
		return ""
	}
	defer file.Close()
	hash := sha256.New()
	if _, err := io.Copy(hash, file); err != nil {
		return ""
	}
	return hex.EncodeToString(hash.Sum(nil))
}

func safePackageName(name string) (string, error) {
	original := strings.TrimSpace(name)
	if original == "" || original == "." || original == ".." || strings.ContainsAny(original, `/\`) {
		return "", errors.New("invalid filename")
	}
	name = filepath.Base(original)
	lower := strings.ToLower(name)
	for _, suffix := range []string{".exe", ".msix", ".appinstaller", ".msi", ".zip", ".7z"} {
		if strings.HasSuffix(lower, suffix) {
			return name, nil
		}
	}
	return "", errors.New("unsupported package extension")
}

func formatBytes(value int64) string {
	if value < 1024 {
		return fmt.Sprintf("%d B", value)
	}
	next := float64(value) / 1024
	for _, unit := range []string{"KB", "MB", "GB"} {
		if next < 1024 || unit == "GB" {
			return fmt.Sprintf("%.1f %s", next, unit)
		}
		next /= 1024
	}
	return fmt.Sprintf("%d B", value)
}

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