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

package licensing

import (
	"encoding/xml"
	"fmt"
	"net/http"
	"strings"

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
	licensing_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licensing"
	repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
	licensing_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/licensing"
)

// Joomla update XML structures.

type xmlUpdates struct {
	XMLName xml.Name    `xml:"updates"`
	Updates []xmlUpdate `xml:"update"`
}

type xmlUpdate struct {
	Name           string        `xml:"name"`
	Element        string        `xml:"element"`
	Type           string        `xml:"type"`
	Version        string        `xml:"version"`
	Tag            string        `xml:"tag"`
	DownloadURL    xmlDownload   `xml:"downloadurl"`
	TargetPlatform xmlTarget     `xml:"targetplatform"`
	PHPMinimum     string        `xml:"php_minimum,omitempty"`
}

type xmlDownload struct {
	Type   string `xml:"type,attr"`
	Format string `xml:"format,attr"`
	URL    string `xml:",chardata"`
}

type xmlTarget struct {
	Name    string `xml:"name,attr"`
	Version string `xml:"version,attr"`
}

// ServeUpdates handles GET /api/v1/licensing/updates/{product}.xml?dlid=XXX&domain=YYY
func ServeUpdates(ctx *context.APIContext) {
	productFile := ctx.PathParam("product")
	productCode := strings.TrimSuffix(productFile, ".xml")
	dlid := ctx.FormString("dlid")
	domain := ctx.FormString("domain")

	// Always return XML content type
	ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")

	// Validation failure → empty <updates/>
	if dlid == "" || !licensing_model.ValidateDLIDFormat(dlid) {
		writeEmptyUpdates(ctx)
		return
	}

	// Look up license
	license, err := licensing_model.GetLicenseByDLID(ctx, dlid)
	if err != nil {
		log.Error("ServeUpdates: GetLicenseByDLID: %v", err)
		writeEmptyUpdates(ctx)
		return
	}
	if license == nil || !license.IsActive() {
		writeEmptyUpdates(ctx)
		return
	}

	// Check entitlement
	hasAccess, err := licensing_model.HasEntitlement(ctx, license.ID, productCode)
	if err != nil {
		log.Error("ServeUpdates: HasEntitlement: %v", err)
		writeEmptyUpdates(ctx)
		return
	}
	if !hasAccess {
		writeEmptyUpdates(ctx)
		return
	}

	// Auto-activate domain
	if domain != "" {
		ip := ctx.Req.RemoteAddr
		if idx := strings.LastIndex(ip, ":"); idx >= 0 {
			ip = ip[:idx]
		}
		_, _, err := licensing_model.ActivateDomain(ctx, license.ID, domain, ip, ctx.FormString("joomla_version"), license.MaxDomains)
		if err != nil {
			if _, ok := err.(licensing_model.ErrDomainLimitReached); ok {
				writeEmptyUpdates(ctx)
				return
			}
			log.Error("ServeUpdates: ActivateDomain: %v", err)
		}
	}

	// Resolve repo from entitlement
	ents, err := licensing_model.GetEntitlementsByLicense(ctx, license.ID)
	if err != nil {
		log.Error("ServeUpdates: GetEntitlementsByLicense: %v", err)
		writeEmptyUpdates(ctx)
		return
	}

	var repoOwner, repoName string
	for _, ent := range ents {
		if ent.ProductCode == productCode {
			repoOwner = ent.RepoOwner
			repoName = ent.RepoName
			break
		}
	}
	if repoName == "" {
		writeEmptyUpdates(ctx)
		return
	}

	// Find the repo
	repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, repoOwner, repoName)
	if err != nil || repo == nil {
		log.Error("ServeUpdates: repo %s/%s not found: %v", repoOwner, repoName, err)
		writeEmptyUpdates(ctx)
		return
	}

	// Get stable release
	releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
		RepoID:        repo.ID,
		ListOptions:   db.ListOptions{PageSize: 1, Page: 1},
		IncludeDrafts: false,
		IncludeTags:   false,
		TagNames:      []string{"stable"},
	})
	if err != nil || len(releases) == 0 {
		// Try latest non-draft release (default order is newest first)
		releases, err = db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
			RepoID:        repo.ID,
			ListOptions:   db.ListOptions{PageSize: 1, Page: 1},
			IncludeDrafts: false,
			IncludeTags:   false,
		})
		if err != nil || len(releases) == 0 {
			writeEmptyUpdates(ctx)
			return
		}
	}

	rel := releases[0]
	version := extractVersion(rel.TagName)
	if version == "" && rel.Title != "" {
		version = extractVersion(rel.Title)
	}
	if version == "" {
		version = rel.TagName
	}

	// Get repo metadata for element name, type, etc.
	manifest, _ := repo_model.GetRepoMetadata(ctx, repo.ID)
	element := strings.ToLower(repoName)
	extType := "package"
	phpMin := "8.1"
	targetVer := "6..*"
	displayName := repoName

	if manifest != nil {
		if e := manifest.FullElementName(); e != "" {
			element = e
		}
		if manifest.ExtensionType != "" {
			extType = manifest.ExtensionType
		}
		if manifest.PHPMinimum != "" {
			phpMin = manifest.PHPMinimum
		}
		if manifest.TargetVersion != "" {
			targetVer = manifest.TargetVersion
		}
		displayName = manifest.DerivedDisplayName()
	}

	// Build signed download URL
	baseURL := setting.AppURL
	token, expires := licensing_service.SignDownloadToken(productCode, version, dlid)
	downloadURL := fmt.Sprintf("%sapi/v1/licensing/download/%s/%s.zip?dlid=%s&token=%s&expires=%d",
		baseURL, productCode, version, dlid, token, expires)

	updates := xmlUpdates{
		Updates: []xmlUpdate{
			{
				Name:    displayName,
				Element: element,
				Type:    extType,
				Version: version,
				Tag:     "stable",
				DownloadURL: xmlDownload{
					Type:   "full",
					Format: "zip",
					URL:    downloadURL,
				},
				TargetPlatform: xmlTarget{
					Name:    "joomla",
					Version: targetVer,
				},
				PHPMinimum: phpMin,
			},
		},
	}

	output, err := xml.MarshalIndent(updates, "", "  ")
	if err != nil {
		log.Error("ServeUpdates: xml.MarshalIndent: %v", err)
		writeEmptyUpdates(ctx)
		return
	}

	ctx.Resp.WriteHeader(http.StatusOK)
	_, _ = ctx.Resp.Write([]byte(xml.Header))
	_, _ = ctx.Resp.Write(output)
}

func writeEmptyUpdates(ctx *context.APIContext) {
	ctx.Resp.WriteHeader(http.StatusOK)
	_, _ = ctx.Resp.Write([]byte(xml.Header + "<updates/>\n"))
}

// extractVersion strips common tag prefixes to get a clean version.
func extractVersion(s string) string {
	v := s
	for _, prefix := range []string{"v", "release-", "release/", "stable-"} {
		v = strings.TrimPrefix(v, prefix)
	}
	return v
}
