// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later

package updateserver

import (
	"context"
	"strings"

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)

func init() {
	db.RegisterModel(new(UpdateStreamConfig))
}

// UpdateStreamConfig stores update stream settings at org or repo level.
// When OwnerID is set and RepoID is 0, it's an org-level default.
// When RepoID is set, it's a per-repo override.
type UpdateStreamConfig struct {
	ID          int64              `xorm:"pk autoincr"`
	OwnerID     int64              `xorm:"INDEX NOT NULL"`         // org or user
	RepoID      int64              `xorm:"INDEX NOT NULL DEFAULT 0"` // 0 = org-level default
	StreamMode  string             `xorm:"NOT NULL DEFAULT 'joomla' 'stream_mode'"` // joomla, custom
	Platform    string             `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, dolibarr, both, wordpress, prestashop, drupal
	LicensingEnabled bool          `xorm:"NOT NULL DEFAULT false 'licensing_enabled'"` // master toggle
	RequireKey  bool               `xorm:"NOT NULL DEFAULT false 'require_key'"`     // require license key for update feed
	FeedVisibility string          `xorm:"VARCHAR(20) NOT NULL DEFAULT 'public' 'feed_visibility'"` // public, no-download, hidden
	DownloadGating string          `xorm:"VARCHAR(20) NOT NULL DEFAULT 'none' 'download_gating'"` // none, all, prerelease
	SupportURL  string             `xorm:"TEXT 'support_url'"`        // wiki or external support page URL
	KeyPrefix   string             `xorm:"VARCHAR(20) 'key_prefix'"`  // org-specific license key prefix (e.g. "ACME")
	// Extension metadata — used in update feed generation.
	ExtensionName    string         `xorm:"TEXT 'extension_name'"`    // element identifier (e.g. pkg_mokowaas, com_mokowaas)
	DisplayName      string         `xorm:"TEXT 'display_name'"`     // human-readable name (e.g. "Package - MokoWaaS")
	Description      string         `xorm:"TEXT"`                     // short description for update feeds
	ExtensionType    string         `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library
	Maintainer       string         `xorm:"TEXT"`                     // maintainer/author name
	MaintainerURL    string         `xorm:"TEXT 'maintainer_url'"`    // maintainer website
	InfoURL          string         `xorm:"TEXT 'info_url'"`          // extension info/product page URL
	TargetVersion    string         `xorm:"TEXT 'target_version'"`    // target platform version regex (e.g. "(5|6)\..*")
	PHPMinimum       string         `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version (e.g. "8.1")
	// CustomStreams is a JSON array of stream definitions.
	// Each entry: {"name":"lts","suffix":"-lts","description":"Long-term support"}
	CustomStreams string            `xorm:"TEXT 'custom_streams'"`
	CreatedUnix  timeutil.TimeStamp `xorm:"INDEX CREATED"`
	UpdatedUnix  timeutil.TimeStamp `xorm:"UPDATED"`
}

func (UpdateStreamConfig) TableName() string {
	return "update_stream_config"
}

// StreamDef defines a single update stream/channel.
type StreamDef struct {
	Name        string `json:"name"`        // e.g. "stable", "lts", "nightly"
	Suffix      string `json:"suffix"`      // tag suffix to match, e.g. "-lts", "-rc"
	Description string `json:"description"` // human-readable label
}

// DefaultJoomlaStreams returns the standard Joomla update streams.
func DefaultJoomlaStreams() []StreamDef {
	return []StreamDef{
		{Name: "stable", Suffix: "", Description: "Stable releases"},
		{Name: "release-candidate", Suffix: "-rc", Description: "Release candidates"},
		{Name: "beta", Suffix: "-beta", Description: "Beta testing"},
		{Name: "alpha", Suffix: "-alpha", Description: "Alpha / early access"},
		{Name: "development", Suffix: "-dev", Description: "Development builds"},
	}
}

// GetCustomStreams parses the CustomStreams JSON field.
func (c *UpdateStreamConfig) GetCustomStreams() []StreamDef {
	if c.CustomStreams == "" {
		return nil
	}
	var streams []StreamDef
	if err := json.Unmarshal([]byte(c.CustomStreams), &streams); err != nil {
		return nil
	}
	return streams
}

// DeleteRepoConfig removes the repo-level update stream config override.
func DeleteRepoConfig(ctx context.Context, repoID int64) error {
	_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(UpdateStreamConfig))
	return err
}

// GetActiveStreams returns the effective streams for this config.
func (c *UpdateStreamConfig) GetActiveStreams() []StreamDef {
	if c.StreamMode == "custom" {
		if custom := c.GetCustomStreams(); len(custom) > 0 {
			return custom
		}
	}
	return DefaultJoomlaStreams()
}

// GetOrgConfig returns the org-level update stream config.
func GetOrgConfig(ctx context.Context, ownerID int64) (*UpdateStreamConfig, error) {
	cfg := new(UpdateStreamConfig)
	has, err := db.GetEngine(ctx).Where("owner_id = ? AND repo_id = 0", ownerID).Get(cfg)
	if err != nil {
		return nil, err
	}
	if !has {
		return &UpdateStreamConfig{OwnerID: ownerID, StreamMode: "joomla"}, nil
	}
	return cfg, nil
}

// GetRepoConfig returns the repo-level override, or nil if none exists.
func GetRepoConfig(ctx context.Context, repoID int64) (*UpdateStreamConfig, error) {
	cfg := new(UpdateStreamConfig)
	has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(cfg)
	if err != nil {
		return nil, err
	}
	if !has {
		return nil, nil
	}
	return cfg, nil
}

// GetEffectiveStreams resolves the streams for a repo: repo override → org default → Joomla default.
func GetEffectiveStreams(ctx context.Context, ownerID, repoID int64) []StreamDef {
	// Check repo-level override first.
	repoCfg, err := GetRepoConfig(ctx, repoID)
	if err == nil && repoCfg != nil {
		return repoCfg.GetActiveStreams()
	}

	// Fall back to org-level config.
	orgCfg, err := GetOrgConfig(ctx, ownerID)
	if err == nil && orgCfg != nil {
		return orgCfg.GetActiveStreams()
	}

	return DefaultJoomlaStreams()
}

// GetEffectiveConfig returns the full config for a repo: repo override → org default.
func GetEffectiveConfig(ctx context.Context, ownerID, repoID int64) *UpdateStreamConfig {
	repoCfg, err := GetRepoConfig(ctx, repoID)
	if err == nil && repoCfg != nil {
		return repoCfg
	}
	orgCfg, err := GetOrgConfig(ctx, ownerID)
	if err == nil && orgCfg != nil {
		return orgCfg
	}
	return nil
}

// SaveConfig creates or updates an update stream config.
func SaveConfig(ctx context.Context, cfg *UpdateStreamConfig) error {
	existing := new(UpdateStreamConfig)
	var has bool
	var err error
	if cfg.RepoID > 0 {
		has, err = db.GetEngine(ctx).Where("repo_id = ?", cfg.RepoID).Get(existing)
	} else {
		has, err = db.GetEngine(ctx).Where("owner_id = ? AND repo_id = 0", cfg.OwnerID).Get(existing)
	}
	if err != nil {
		return err
	}

	if has {
		cfg.ID = existing.ID
		_, err = db.GetEngine(ctx).ID(cfg.ID).AllCols().Update(cfg)
	} else {
		_, err = db.GetEngine(ctx).Insert(cfg)
	}
	return err
}

// MatchStreamFromTag determines which stream a tag belongs to based on the given stream definitions.
// Supports two conventions:
//   - Stream-name tags: tag IS the stream name (e.g. "stable", "release-candidate", "development")
//   - Version tags: tag contains a version + optional suffix (e.g. "v1.0.0", "v1.0.0-rc1")
func MatchStreamFromTag(tagName string, isPrerelease bool, streams []StreamDef) string {
	lower := strings.ToLower(tagName)

	// First: check if the tag name directly matches a stream name (stream-name convention).
	for _, s := range streams {
		if strings.EqualFold(s.Name, tagName) {
			return s.Name
		}
	}

	// Second: check suffixes in the tag (version-tag convention, longest match first).
	var bestMatch string
	bestLen := 0
	for _, s := range streams {
		if s.Suffix == "" {
			continue // stable/default stream handled below
		}
		if strings.Contains(lower, s.Suffix) && len(s.Suffix) > bestLen {
			bestMatch = s.Name
			bestLen = len(s.Suffix)
		}
	}
	if bestMatch != "" {
		return bestMatch
	}

	// If prerelease and no suffix matched, use the first prerelease stream.
	if isPrerelease {
		for _, s := range streams {
			if s.Suffix != "" {
				return s.Name
			}
		}
	}

	// Default: first stream with empty suffix (stable).
	for _, s := range streams {
		if s.Suffix == "" {
			return s.Name
		}
	}
	return "stable"
}
