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

package repo

import (
	"net/http"
	"strconv"
	"strings"
	"time"

	updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
	repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
	unit_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)

const tplLicenses templates.TplName = "repo/licenses"

// parseAllowedChannels splits an AllowedChannels string (CSV or JSON array) into a slice.
func parseAllowedChannels(s string) []string {
	if s == "" {
		return nil
	}
	if strings.HasPrefix(s, "[") {
		var parsed []string
		if err := json.Unmarshal([]byte(s), &parsed); err == nil {
			return parsed
		}
	}
	parts := strings.Split(s, ",")
	result := make([]string, 0, len(parts))
	for _, p := range parts {
		if t := strings.TrimSpace(p); t != "" {
			result = append(result, t)
		}
	}
	return result
}

// ChannelItem represents a selectable channel for the combolist template.
type ChannelItem struct {
	Value string
	Label string
}

// buildChannelItems converts stream definitions into combolist items.
func buildChannelItems(streams []updateserver_model.StreamDef) []ChannelItem {
	items := make([]ChannelItem, 0, len(streams))
	for _, s := range streams {
		label := s.Name
		if s.Description != "" {
			label = s.Name + " — " + s.Description
		}
		items = append(items, ChannelItem{Value: s.Name, Label: label})
	}
	return items
}

// LicensePackageDisplay is used in templates.
type LicensePackageDisplay struct {
	*updateserver_model.LicensePackage
	KeyCount int64
	Created  time.Time
}

// Licenses shows the license packages and keys for a repo.
func Licenses(ctx *context.Context) {
	ctx.Data["Title"] = ctx.Tr("repo.licenses")
	ctx.Data["PageIsLicenses"] = true
	ctx.Data["IsLicensesPage"] = true
	canWriteLicenses := ctx.Repo.Permission.CanWrite(unit_model.TypeLicenses) || ctx.IsUserSiteAdmin()
	ctx.Data["IsRepoAdmin"] = canWriteLicenses
	ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()

	ownerID := ctx.Repo.Repository.OwnerID

	// Auto-create master package + key if admin and none exist.
	if canWriteLicenses {
		newMasterKey, err := updateserver_model.EnsureMasterKey(ctx, ownerID)
		if err != nil {
			ctx.ServerError("EnsureMasterKey", err)
			return
		}
		if newMasterKey != "" {
			ctx.Data["NewMasterKey"] = newMasterKey
		}
	}

	// Always load the master key for display (prefix + status).
	masterKey, _ := updateserver_model.GetMasterKey(ctx, ownerID)
	ctx.Data["MasterKey"] = masterKey

	pkgs, err := updateserver_model.ListLicensePackages(ctx, ownerID)
	if err != nil {
		ctx.ServerError("ListLicensePackages", err)
		return
	}

	var display []LicensePackageDisplay
	for _, pkg := range pkgs {
		count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
		display = append(display, LicensePackageDisplay{
			LicensePackage: pkg,
			KeyCount:       count,
			Created:        time.Unix(int64(pkg.CreatedUnix), 0),
		})
	}
	ctx.Data["LicensePackages"] = display

	// Search or list keys.
	searchQuery := strings.TrimSpace(ctx.FormString("q"))
	ctx.Data["SearchQuery"] = searchQuery

	var keys []*updateserver_model.LicenseKey
	if searchQuery != "" {
		keys, err = updateserver_model.SearchLicenseKeys(ctx, ownerID, searchQuery)
	} else {
		keys, err = updateserver_model.ListLicenseKeys(ctx, ownerID)
	}
	if err != nil {
		ctx.ServerError("ListLicenseKeys", err)
		return
	}
	ctx.Data["LicenseKeys"] = keys
	ctx.Data["CanDelete"] = canDeleteLicenses(ctx)

	// Load archived packages.
	archivedPkgs, _ := updateserver_model.ListArchivedLicensePackages(ctx, ownerID)
	var archivedDisplay []LicensePackageDisplay
	for _, pkg := range archivedPkgs {
		count, _ := updateserver_model.CountKeysByPackage(ctx, pkg.ID)
		archivedDisplay = append(archivedDisplay, LicensePackageDisplay{
			LicensePackage: pkg,
			KeyCount:       count,
			Created:        time.Unix(int64(pkg.CreatedUnix), 0),
		})
	}
	ctx.Data["ArchivedPackages"] = archivedDisplay

	// Load available streams for the channels combolist.
	orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
	var streams []updateserver_model.StreamDef
	if orgCfg != nil {
		streams = orgCfg.GetActiveStreams()
	} else {
		streams = updateserver_model.DefaultJoomlaStreams()
	}
	ctx.Data["AvailableStreams"] = streams
	ctx.Data["ChannelItems"] = buildChannelItems(streams)

	// Load org repos for repo scope selector.
	orgRepos, _ := repo_model.GetOrgRepositories(ctx, ownerID)
	ctx.Data["OrgRepos"] = orgRepos

	ctx.HTML(http.StatusOK, tplLicenses)
}

// LicensesCreatePackage handles POST to create a new license package.
func LicensesCreatePackage(ctx *context.Context) {
	name := ctx.FormString("name")
	if name == "" {
		ctx.Flash.Error("Package name is required")
		ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
		return
	}

	durationDays, _ := strconv.Atoi(ctx.FormString("duration_days"))
	maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
	domainLockHours, _ := strconv.Atoi(ctx.FormString("domain_lock_hours"))

	channelStr := ctx.FormString("allowed_channels")
	var allowedChannels string
	if channelStr != "" {
		// Combolist sends comma-separated values; store as JSON array.
		parts := strings.Split(channelStr, ",")
		data, _ := json.Marshal(parts)
		allowedChannels = string(data)
	}

	repoScope := ctx.FormString("repo_scope")
	if repoScope == "" {
		repoScope = "all"
	}

	pkg := &updateserver_model.LicensePackage{
		OwnerID:           ctx.Repo.Repository.OwnerID,
		Name:              name,
		Description:       ctx.FormString("description"),
		DurationDays:      durationDays,
		MaxSites:          maxSites,
		DomainLockHours:   domainLockHours,
		AllowedChannels:   allowedChannels,
		DomainRestriction: strings.TrimSpace(ctx.FormString("domain_restriction")),
		RepoScope:         repoScope,
		IsActive:          true,
	}

	if err := updateserver_model.CreateLicensePackage(ctx, pkg); err != nil {
		ctx.ServerError("CreateLicensePackage", err)
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.licenses.package_created"))
	ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}

// LicensesRegenerateMasterKey handles POST to regenerate the master key.
func LicensesRegenerateMasterKey(ctx *context.Context) {
	ownerID := ctx.Repo.Repository.OwnerID

	// Deactivate the old master key.
	oldKey, _ := updateserver_model.GetMasterKey(ctx, ownerID)
	if oldKey != nil {
		oldKey.IsActive = false
		_ = updateserver_model.UpdateLicenseKey(ctx, oldKey)
	}

	// Find the master package.
	pkgs, err := updateserver_model.ListLicensePackages(ctx, ownerID)
	if err != nil {
		ctx.ServerError("ListLicensePackages", err)
		return
	}
	var masterPkg *updateserver_model.LicensePackage
	for _, pkg := range pkgs {
		if pkg.Name == updateserver_model.MasterPackageName {
			masterPkg = pkg
			break
		}
	}
	if masterPkg == nil {
		ctx.Flash.Error("Master package not found")
		ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
		return
	}

	// Create a new master key.
	newKey := &updateserver_model.LicenseKey{
		PackageID:  masterPkg.ID,
		OwnerID:    ownerID,
		IsInternal: true,
		IsActive:   true,
	}
	rawKey, err := updateserver_model.CreateLicenseKey(ctx, newKey)
	if err != nil {
		ctx.ServerError("CreateLicenseKey", err)
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.licenses.master_key_regenerated"))
	ctx.Data["NewMasterKey"] = rawKey
	Licenses(ctx)
}

// LicensesGenerateKey handles POST to generate a new key from a package.
func LicensesGenerateKey(ctx *context.Context) {
	// Accept package_id from form body or query string (modal sets it via form action URL).
	pkgIDStr := ctx.FormString("package_id")
	if pkgIDStr == "" {
		pkgIDStr = ctx.Req.URL.Query().Get("package_id")
	}
	packageID, _ := strconv.ParseInt(pkgIDStr, 10, 64)
	if packageID == 0 {
		ctx.Flash.Error("Invalid package")
		ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
		return
	}

	pkg, err := updateserver_model.GetLicensePackageByID(ctx, packageID)
	if err != nil {
		ctx.ServerError("GetLicensePackageByID", err)
		return
	}

	// Domain restriction: use form input, fall back to package default.
	domainRestriction := strings.TrimSpace(ctx.FormString("domain_restriction"))
	if domainRestriction == "" && pkg.DomainRestriction != "" {
		domainRestriction = pkg.DomainRestriction
	}

	key := &updateserver_model.LicenseKey{
		PackageID:         packageID,
		OwnerID:           ctx.Repo.Repository.OwnerID,
		IsActive:          true,
		DomainRestriction: domainRestriction,
		LicenseeName:      strings.TrimSpace(ctx.FormString("licensee_name")),
		LicenseeEmail:     strings.TrimSpace(ctx.FormString("licensee_email")),
	}

	// Auto-calculate expiry from package duration.
	if pkg.DurationDays > 0 {
		expires := time.Now().AddDate(0, 0, pkg.DurationDays)
		key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
	}

	// Site admins and org owners can manually set a custom key.
	var rawKey string
	customKey := strings.TrimSpace(ctx.FormString("custom_key"))
	if customKey != "" && (ctx.IsUserSiteAdmin() || ctx.Repo.Permission.IsOwner()) {
		if err := updateserver_model.CreateLicenseKeyCustom(ctx, key, customKey); err != nil {
			ctx.ServerError("CreateLicenseKeyCustom", err)
			return
		}
		rawKey = customKey
	} else {
		rawKey, err = updateserver_model.CreateLicenseKey(ctx, key)
		if err != nil {
			ctx.ServerError("CreateLicenseKey", err)
			return
		}
	}

	ctx.Data["Title"] = ctx.Tr("repo.licenses")
	ctx.Data["PageIsLicenses"] = true
	ctx.Data["IsLicensesPage"] = true
	ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.CanWrite(unit_model.TypeLicenses) || ctx.IsUserSiteAdmin()
	ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()
	ctx.Data["NewKeyCreated"] = rawKey

	// Re-render the page with the new key displayed.
	ownerID := ctx.Repo.Repository.OwnerID
	pkgs, _ := updateserver_model.ListLicensePackages(ctx, ownerID)
	var display []LicensePackageDisplay
	for _, p := range pkgs {
		count, _ := updateserver_model.CountKeysByPackage(ctx, p.ID)
		display = append(display, LicensePackageDisplay{
			LicensePackage: p,
			KeyCount:       count,
			Created:        time.Unix(int64(p.CreatedUnix), 0),
		})
	}
	ctx.Data["LicensePackages"] = display
	keys, _ := updateserver_model.ListLicenseKeys(ctx, ownerID)
	ctx.Data["LicenseKeys"] = keys

	orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
	var genStreams []updateserver_model.StreamDef
	if orgCfg != nil {
		genStreams = orgCfg.GetActiveStreams()
	} else {
		genStreams = updateserver_model.DefaultJoomlaStreams()
	}
	ctx.Data["AvailableStreams"] = genStreams
	ctx.Data["ChannelItems"] = buildChannelItems(genStreams)

	ctx.HTML(http.StatusOK, tplLicenses)
}

// LicensesRevokeKey handles POST to revoke a license key.
func LicensesRevokeKey(ctx *context.Context) {
	keyID := ctx.PathParamInt64("id")
	key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
	if err != nil {
		ctx.ServerError("GetLicenseKeyByID", err)
		return
	}

	key.IsActive = false
	if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
		ctx.ServerError("UpdateLicenseKey", err)
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.licenses.key_revoked"))
	ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}

const tplLicensesEditPackage templates.TplName = "repo/licenses_edit_package"
const tplLicensesEditKey templates.TplName = "repo/licenses_edit_key"

// LicensesEditKey shows the edit form for a license key.
func LicensesEditKey(ctx *context.Context) {
	keyID := ctx.PathParamInt64("id")
	key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
	if err != nil {
		ctx.ServerError("GetLicenseKeyByID", err)
		return
	}

	if key.IsInternal {
		ctx.Flash.Error("Master keys cannot be edited")
		ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
		return
	}

	ctx.Data["Title"] = ctx.Tr("repo.licenses.edit_key")
	ctx.Data["PageIsLicenses"] = true
	ctx.Data["IsLicensesPage"] = true
	ctx.Data["Key"] = key
	ctx.Data["FormAction"] = ctx.Repo.RepoLink + "/licenses/keys/" + strconv.FormatInt(key.ID, 10) + "/edit"
	ctx.Data["BackLink"] = ctx.Repo.RepoLink + "/licenses"

	if key.ExpiresUnix > 0 {
		ctx.Data["ExpiresDate"] = time.Unix(int64(key.ExpiresUnix), 0).Format("2006-01-02")
	}

	ctx.HTML(http.StatusOK, tplLicensesEditKey)
}

// LicensesEditKeyPost saves edits to a license key.
func LicensesEditKeyPost(ctx *context.Context) {
	keyID := ctx.PathParamInt64("id")
	key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
	if err != nil {
		ctx.ServerError("GetLicenseKeyByID", err)
		return
	}

	if key.IsInternal {
		ctx.Flash.Error("Master keys cannot be edited")
		ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
		return
	}

	key.LicenseeName = ctx.FormString("licensee_name")
	key.LicenseeEmail = ctx.FormString("licensee_email")
	key.DomainRestriction = ctx.FormString("domain_restriction")
	maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
	key.MaxSites = maxSites
	key.IsActive = ctx.FormString("is_active") == "on"

	expiresStr := ctx.FormString("expires_at")
	if expiresStr != "" {
		t, err := time.Parse("2006-01-02", expiresStr)
		if err == nil {
			key.ExpiresUnix = timeutil.TimeStamp(t.Unix())
		}
	} else {
		key.ExpiresUnix = 0
	}

	if err := updateserver_model.UpdateLicenseKey(ctx, key); err != nil {
		ctx.ServerError("UpdateLicenseKey", err)
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.licenses.key_updated"))
	ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}

// LicensesEditPackage shows the edit form for a license package.
func LicensesEditPackage(ctx *context.Context) {
	pkgID := ctx.PathParamInt64("id")
	pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
	if err != nil {
		ctx.ServerError("GetLicensePackageByID", err)
		return
	}

	if pkg.Name == updateserver_model.MasterPackageName {
		ctx.Flash.Error("Master package cannot be edited")
		ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
		return
	}

	ctx.Data["Title"] = ctx.Tr("repo.licenses.edit_package")
	ctx.Data["PageIsLicenses"] = true
	ctx.Data["IsLicensesPage"] = true
	ctx.Data["Package"] = pkg
	selectedChannels := parseAllowedChannels(pkg.AllowedChannels)
	ctx.Data["SelectedChannels"] = selectedChannels

	ownerID := ctx.Repo.Repository.OwnerID
	orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ownerID)
	var editStreams []updateserver_model.StreamDef
	if orgCfg != nil {
		editStreams = orgCfg.GetActiveStreams()
	} else {
		editStreams = updateserver_model.DefaultJoomlaStreams()
	}
	ctx.Data["AvailableStreams"] = editStreams
	ctx.Data["ChannelItems"] = buildChannelItems(editStreams)
	ctx.Data["SelectedChannelValues"] = strings.Join(selectedChannels, ",")

	ctx.HTML(http.StatusOK, tplLicensesEditPackage)
}

// LicensesEditPackagePost saves edits to a license package.
func LicensesEditPackagePost(ctx *context.Context) {
	pkgID := ctx.PathParamInt64("id")
	pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
	if err != nil {
		ctx.ServerError("GetLicensePackageByID", err)
		return
	}

	if pkg.Name == updateserver_model.MasterPackageName {
		ctx.Flash.Error("Master package cannot be edited")
		ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
		return
	}

	pkg.Name = ctx.FormString("name")
	pkg.Description = ctx.FormString("description")
	durationDays, _ := strconv.Atoi(ctx.FormString("duration_days"))
	pkg.DurationDays = durationDays
	maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
	pkg.MaxSites = maxSites
	domainLockHours, _ := strconv.Atoi(ctx.FormString("domain_lock_hours"))
	pkg.DomainLockHours = domainLockHours
	repoScope := ctx.FormString("repo_scope")
	if repoScope != "" {
		pkg.RepoScope = repoScope
	}

	channels := ctx.Req.Form["allowed_channels"]
	if len(channels) > 0 {
		data, _ := json.Marshal(channels)
		pkg.AllowedChannels = string(data)
	} else {
		pkg.AllowedChannels = ""
	}

	pkg.DomainRestriction = strings.TrimSpace(ctx.FormString("domain_restriction"))
	pkg.IsActive = ctx.FormString("is_active") == "on"

	if err := updateserver_model.UpdateLicensePackage(ctx, pkg); err != nil {
		ctx.ServerError("UpdateLicensePackage", err)
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.licenses.package_updated"))
	ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}

// canDeleteLicenses returns true if the user is a site admin or repo owner.
func canDeleteLicenses(ctx *context.Context) bool {
	return ctx.IsUserSiteAdmin() || ctx.Repo.Permission.IsOwner()
}

// LicensesArchivePackage archives a license package.
func LicensesArchivePackage(ctx *context.Context) {
	pkgID := ctx.PathParamInt64("id")
	pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
	if err != nil {
		ctx.ServerError("GetLicensePackageByID", err)
		return
	}
	if pkg.Name == updateserver_model.MasterPackageName {
		ctx.Flash.Error("Master package cannot be archived")
		ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
		return
	}
	if err := updateserver_model.ArchiveLicensePackage(ctx, pkgID); err != nil {
		ctx.ServerError("ArchiveLicensePackage", err)
		return
	}
	ctx.Flash.Success(ctx.Tr("repo.licenses.package_archived"))
	ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}

// LicensesUnarchivePackage removes archive status from a package.
func LicensesUnarchivePackage(ctx *context.Context) {
	pkgID := ctx.PathParamInt64("id")
	if err := updateserver_model.UnarchiveLicensePackage(ctx, pkgID); err != nil {
		ctx.ServerError("UnarchiveLicensePackage", err)
		return
	}
	ctx.Flash.Success(ctx.Tr("repo.licenses.package_unarchived"))
	ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}

// LicensesDeletePackage permanently deletes a license package. Site admin or repo owner.
func LicensesDeletePackage(ctx *context.Context) {
	if !canDeleteLicenses(ctx) {
		ctx.NotFound(nil)
		return
	}
	pkgID := ctx.PathParamInt64("id")
	pkg, err := updateserver_model.GetLicensePackageByID(ctx, pkgID)
	if err != nil {
		ctx.ServerError("GetLicensePackageByID", err)
		return
	}
	if pkg.Name == updateserver_model.MasterPackageName {
		ctx.Flash.Error("Master package cannot be deleted")
		ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
		return
	}
	if err := updateserver_model.DeleteLicensePackage(ctx, pkgID); err != nil {
		ctx.ServerError("DeleteLicensePackage", err)
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.licenses.package_deleted"))
	ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}

// LicensesRenewKey extends a license key's expiration by the package's duration.
func LicensesRenewKey(ctx *context.Context) {
	keyID := ctx.PathParamInt64("id")
	key, err := updateserver_model.GetLicenseKeyByID(ctx, keyID)
	if err != nil {
		ctx.ServerError("GetLicenseKeyByID", err)
		return
	}

	pkg, err := updateserver_model.GetLicensePackageByID(ctx, key.PackageID)
	if err != nil {
		ctx.ServerError("GetLicensePackageByID", err)
		return
	}

	days := pkg.DurationDays
	if days == 0 {
		days = 365 // default to 1 year for lifetime packages
	}

	if err := updateserver_model.RenewLicenseKey(ctx, keyID, days); err != nil {
		ctx.ServerError("RenewLicenseKey", err)
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.licenses.key_renewed", days))
	ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}

// LicensesDeleteKey permanently deletes a license key. Site admin or repo owner.
func LicensesDeleteKey(ctx *context.Context) {
	if !canDeleteLicenses(ctx) {
		ctx.NotFound(nil)
		return
	}
	keyID := ctx.PathParamInt64("id")
	if err := updateserver_model.DeleteLicenseKey(ctx, keyID); err != nil {
		ctx.ServerError("DeleteLicenseKey", err)
		return
	}
	ctx.Flash.Success(ctx.Tr("repo.licenses.key_deleted"))
	ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
}
