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

package org

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

	updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
	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 tplOrgLicenses templates.TplName = "org/licenses"

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

func buildOrgChannelItems(streams []updateserver_model.StreamDef) []OrgChannelItem {
	items := make([]OrgChannelItem, 0, len(streams))
	for _, s := range streams {
		label := s.Name
		if s.Description != "" {
			label = s.Name + " — " + s.Description
		}
		items = append(items, OrgChannelItem{Value: s.Name, Label: label})
	}
	return items
}

// parseOrgAllowedChannels splits an AllowedChannels string (CSV or JSON array) into a slice.
func parseOrgAllowedChannels(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
}

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

// Licenses shows the org-level license packages and keys.
func Licenses(ctx *context.Context) {
	ctx.Data["Title"] = ctx.Tr("repo.licenses")
	ctx.Data["IsLicensesPage"] = true

	org := ctx.Org.Organization
	ownerID := org.ID

	canWriteLicenses := ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unit_model.TypeLicenses) >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()

	// Auto-create master key if has write access.
	if canWriteLicenses {
		newMasterKey, err := updateserver_model.EnsureMasterKey(ctx, ownerID)
		if err != nil {
			ctx.ServerError("EnsureMasterKey", err)
			return
		}
		if newMasterKey != "" {
			ctx.Data["NewMasterKey"] = newMasterKey
		}
	}

	pkgs, err := updateserver_model.ListLicensePackagesWithAncestors(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.SearchLicenseKeysWithAncestors(ctx, ownerID, searchQuery)
	} else {
		keys, err = updateserver_model.ListLicenseKeysWithAncestors(ctx, ownerID)
	}
	if err != nil {
		ctx.ServerError("ListLicenseKeys", err)
		return
	}
	ctx.Data["LicenseKeys"] = keys
	ctx.Data["IsRepoAdmin"] = canWriteLicenses
	ctx.Data["IsSiteAdmin"] = ctx.IsUserSiteAdmin()
	ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
	ctx.Data["CanDelete"] = canOrgDeleteLicenses(ctx)
	ctx.Data["OrgLicensingEnabled"] = true

	// 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

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

	orgRepos, _ := repo_model.GetOrgRepositories(ctx, ownerID)
	ctx.Data["OrgRepos"] = orgRepos

	ctx.HTML(http.StatusOK, tplOrgLicenses)
}

// LicensesCreatePackage handles POST to create a new org-level license package.
func LicensesCreatePackage(ctx *context.Context) {
	name := ctx.FormString("name")
	if name == "" {
		ctx.Flash.Error("Package name is required")
		ctx.Redirect(ctx.Org.OrgLink + "/-/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 != "" {
		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.Org.Organization.ID,
		Name:            name,
		Description:     ctx.FormString("description"),
		DurationDays:    durationDays,
		MaxSites:        maxSites,
		DomainLockHours: domainLockHours,
		AllowedChannels: allowedChannels,
		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.Org.OrgLink + "/-/licenses")
}

// LicensesGenerateKey handles POST to generate a key from an org package.
func LicensesGenerateKey(ctx *context.Context) {
	packageID, _ := strconv.ParseInt(ctx.FormString("package_id"), 10, 64)
	if packageID == 0 {
		ctx.Flash.Error("Invalid package")
		ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
		return
	}

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

	key := &updateserver_model.LicenseKey{
		PackageID: packageID,
		OwnerID:   ctx.Org.Organization.ID,
		IsActive:  true,
	}

	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.Org.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
		}
	}

	// Re-render with the new key shown.
	ctx.Data["Title"] = ctx.Tr("repo.licenses")
	ctx.Data["IsLicensesPage"] = true
	ctx.Data["IsRepoAdmin"] = ctx.Org.IsOwner
	ctx.Data["NewKeyCreated"] = rawKey

	ownerID := ctx.Org.Organization.ID
	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)
	if orgCfg != nil {
		ctx.Data["AvailableStreams"] = orgCfg.GetActiveStreams()
	} else {
		ctx.Data["AvailableStreams"] = updateserver_model.DefaultJoomlaStreams()
	}

	ctx.HTML(http.StatusOK, tplOrgLicenses)
}

const tplOrgLicensesEditPackage templates.TplName = "org/licenses_edit_package"
const tplOrgLicensesEditKey templates.TplName = "repo/licenses_edit_key"

// LicensesEditPackage shows the edit form for an org 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.Org.OrgLink + "/-/licenses")
		return
	}

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

	orgCfg, _ := updateserver_model.GetOrgConfig(ctx, ctx.Org.Organization.ID)
	var editStreams []updateserver_model.StreamDef
	if orgCfg != nil {
		editStreams = orgCfg.GetActiveStreams()
	} else {
		editStreams = updateserver_model.DefaultJoomlaStreams()
	}
	ctx.Data["AvailableStreams"] = editStreams
	ctx.Data["ChannelItems"] = buildOrgChannelItems(editStreams)
	ctx.Data["SelectedChannelValues"] = strings.Join(selectedChannels, ",")

	ctx.HTML(http.StatusOK, tplOrgLicensesEditPackage)
}

// LicensesEditPackagePost saves edits to an org 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.Org.OrgLink + "/-/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

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

	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.Org.OrgLink + "/-/licenses")
}

// canOrgDeleteLicenses returns true if the user is a site admin or org owner.
func canOrgDeleteLicenses(ctx *context.Context) bool {
	return ctx.IsUserSiteAdmin() || ctx.Org.IsOwner
}

// LicensesArchivePackage archives an org 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.Org.OrgLink + "/-/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.Org.OrgLink + "/-/licenses")
}

// LicensesUnarchivePackage restores an archived org license 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.Org.OrgLink + "/-/licenses")
}

// LicensesDeletePackage permanently deletes an org license package. Site admin or org owner.
func LicensesDeletePackage(ctx *context.Context) {
	if !canOrgDeleteLicenses(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.Org.OrgLink + "/-/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.Org.OrgLink + "/-/licenses")
}

// LicensesEditKey shows the edit form for an org 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.Org.OrgLink + "/-/licenses")
		return
	}

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

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

	ctx.HTML(http.StatusOK, tplOrgLicensesEditKey)
}

// LicensesEditKeyPost saves edits to an org 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.Org.OrgLink + "/-/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.Org.OrgLink + "/-/licenses")
}

// LicensesRevokeKey handles POST to revoke an org 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.Org.OrgLink + "/-/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.Org.OrgLink + "/-/licenses")
}

// LicensesDeleteKey permanently deletes a license key. Site admin or org owner.
func LicensesDeleteKey(ctx *context.Context) {
	if !canOrgDeleteLicenses(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.Org.OrgLink + "/-/licenses")
}
