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

package repo

import (
	"net/http"
	"strings"

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/updateserver"
)

// validateUpdateKey checks for a license key in the request and validates it.
// Returns allowed channels (nil = all channels), whether access is granted,
// and whether download URLs should be stripped (for "no-download" feed visibility).
func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool, stripDownloads bool) {
	rawKey := ctx.FormString("key")
	if rawKey == "" {
		rawKey = ctx.FormString("download_key")
	}
	if rawKey == "" {
		rawKey = ctx.FormString("dlid")
	}

	if rawKey == "" {
		// Feed is always public — shows versions and download URLs.
		// Actual file downloads are gated by CheckDownloadGating, not the feed.
		return nil, true, false
	}

	domain := ctx.FormString("domain")
	key, pkg, err := licenses.ValidateLicenseKeyForRepo(ctx, rawKey, domain, ctx.Repo.Repository.ID)
	if err != nil {
		log.Debug("License key validation failed: %v", err)
		return nil, false, false
	}

	// Update heartbeat and record usage.
	_ = licenses.TouchHeartbeat(ctx, key.ID)
	_ = licenses.RecordUsage(ctx, &licenses.LicenseKeyUsage{
		KeyID:       key.ID,
		RepoID:      ctx.Repo.Repository.ID,
		Domain:      domain,
		IPAddress:   ctx.RemoteAddr(),
		UserAgent:   ctx.Req.UserAgent(),
		VersionFrom: ctx.FormString("version"),
	})

	// Parse allowed channels from the package.
	if pkg.AllowedChannels != "" {
		var channels []string
		if strings.HasPrefix(pkg.AllowedChannels, "[") {
			// JSON array format — parse first to avoid substring issues.
			if err := json.Unmarshal([]byte(pkg.AllowedChannels), &channels); err != nil {
				channels = strings.Split(pkg.AllowedChannels, ",")
			}
		} else {
			channels = strings.Split(pkg.AllowedChannels, ",")
		}
		for i := range channels {
			channels[i] = strings.TrimSpace(channels[i])
		}
		// Normalize shorthand names to full Joomla convention.
		for i := range channels {
			channels[i] = updateserver.NormalizeChannel(channels[i])
		}
		return channels, true, false
	}

	// Master/internal keys or packages with no channel restriction — all channels.
	return nil, true, false
}

// ServeUpdatesXML generates and serves a Joomla-compatible updates.xml
// from the repository's releases.
func ServeUpdatesXML(ctx *context.Context) {
	// Block if platform doesn't include joomla.
	platform := ctx.Data["RepoUpdatePlatform"]
	if platform == "dolibarr" {
		ctx.NotFound(nil)
		return
	}

	allowedChannels, ok, stripDownloads := validateUpdateKey(ctx)
	if !ok {
		ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
		ctx.Resp.WriteHeader(http.StatusOK)
		_, _ = ctx.Resp.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><updates></updates>`))
		return
	}

	repoCfg, _ := licenses.GetRepoConfig(ctx, ctx.Repo.Repository.ID)
	// Show <downloadkey> only when downloads are gated (prerelease or all).
	// No gating = no license keys needed = no downloadkey element.
	requireKey := repoCfg != nil && repoCfg.DownloadGating != "" && repoCfg.DownloadGating != "none"

	xmlData, err := updateserver.GenerateJoomlaXML(ctx, ctx.Repo.Repository, requireKey, stripDownloads, allowedChannels...)
	if err != nil {
		ctx.ServerError("GenerateJoomlaXML", err)
		return
	}

	ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
	ctx.Resp.WriteHeader(http.StatusOK)
	_, _ = ctx.Resp.Write(xmlData)
}

// ServeDolibarrJSON generates and serves a Dolibarr-compatible update feed
// from the repository's releases. Uses the same license key validation as the
// Joomla XML feed — all platforms share the same licensing system.
func ServeDolibarrJSON(ctx *context.Context) {
	// Block if platform doesn't include dolibarr.
	platform := ctx.Data["RepoUpdatePlatform"]
	if platform == "joomla" || platform == "" {
		ctx.NotFound(nil)
		return
	}

	allowedChannels, ok, _ := validateUpdateKey(ctx)
	if !ok {
		ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
		ctx.Resp.WriteHeader(http.StatusOK)
		_, _ = ctx.Resp.Write([]byte(`{"module":"","updates":[]}`))
		return
	}

	data, err := updateserver.GenerateDolibarrJSON(ctx, ctx.Repo.Repository, allowedChannels...)
	if err != nil {
		ctx.ServerError("GenerateDolibarrJSON", err)
		return
	}

	jsonData, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		ctx.ServerError("json.Marshal", err)
		return
	}

	ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
	ctx.Resp.WriteHeader(http.StatusOK)
	_, _ = ctx.Resp.Write(jsonData)
}

// ServeWordPressJSON generates and serves a WordPress PUC-compatible update feed.
// Compatible with the YahnisElsts plugin-update-checker library.
func ServeWordPressJSON(ctx *context.Context) {
	// Block if platform doesn't include wordpress.
	platform := ctx.Data["RepoUpdatePlatform"]
	if platform != "wordpress" && platform != "both" {
		ctx.NotFound(nil)
		return
	}

	_, ok, stripDownloads := validateUpdateKey(ctx)
	if !ok {
		ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
		ctx.Resp.WriteHeader(http.StatusOK)
		_, _ = ctx.Resp.Write([]byte(`{"name":"","slug":"","version":"0.0.0"}`))
		return
	}

	// Extract license key for embedding in download URL.
	licenseKey := ctx.FormString("license_key")
	if licenseKey == "" {
		licenseKey = ctx.FormString("dlid")
	}
	if licenseKey == "" {
		licenseKey = ctx.FormString("key")
	}
	if stripDownloads {
		licenseKey = "" // strip download URLs by clearing the key
	}

	data, err := updateserver.GenerateWordPressJSON(ctx, ctx.Repo.Repository, licenseKey)
	if err != nil {
		ctx.ServerError("GenerateWordPressJSON", err)
		return
	}

	jsonData, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		ctx.ServerError("json.Marshal", err)
		return
	}

	ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
	ctx.Resp.WriteHeader(http.StatusOK)
	_, _ = ctx.Resp.Write(jsonData)
}

// ServeComposerJSON generates and serves a Composer packages.json feed.
func ServeComposerJSON(ctx *context.Context) {
	platform := ctx.Data["RepoUpdatePlatform"]
	if platform != "composer" {
		ctx.NotFound(nil)
		return
	}

	_, ok, _ := validateUpdateKey(ctx)
	if !ok {
		ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
		ctx.Resp.WriteHeader(http.StatusOK)
		_, _ = ctx.Resp.Write([]byte(`{"packages":{}}`))
		return
	}

	licenseKey := ctx.FormString("key")
	if licenseKey == "" {
		licenseKey = ctx.FormString("dlid")
	}

	data, err := updateserver.GenerateComposerJSON(ctx, ctx.Repo.Repository, licenseKey)
	if err != nil {
		ctx.ServerError("GenerateComposerJSON", err)
		return
	}

	jsonData, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		ctx.ServerError("json.Marshal", err)
		return
	}

	ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
	ctx.Resp.WriteHeader(http.StatusOK)
	_, _ = ctx.Resp.Write(jsonData)
}

// ServePrestaShopXML generates and serves a PrestaShop module update XML.
func ServePrestaShopXML(ctx *context.Context) {
	platform := ctx.Data["RepoUpdatePlatform"]
	if platform != "prestashop" {
		ctx.NotFound(nil)
		return
	}

	allowedChannels, ok, _ := validateUpdateKey(ctx)
	if !ok {
		ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
		ctx.Resp.WriteHeader(http.StatusOK)
		_, _ = ctx.Resp.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><modules></modules>`))
		return
	}

	xmlData, err := updateserver.GeneratePrestaShopXML(ctx, ctx.Repo.Repository, allowedChannels...)
	if err != nil {
		ctx.ServerError("GeneratePrestaShopXML", err)
		return
	}

	ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
	ctx.Resp.WriteHeader(http.StatusOK)
	_, _ = ctx.Resp.Write(xmlData)
}

// ServeDrupalXML generates and serves a Drupal update status XML.
func ServeDrupalXML(ctx *context.Context) {
	platform := ctx.Data["RepoUpdatePlatform"]
	if platform != "drupal" {
		ctx.NotFound(nil)
		return
	}

	allowedChannels, ok, _ := validateUpdateKey(ctx)
	if !ok {
		ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
		ctx.Resp.WriteHeader(http.StatusOK)
		_, _ = ctx.Resp.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><project></project>`))
		return
	}

	xmlData, err := updateserver.GenerateDrupalXML(ctx, ctx.Repo.Repository, allowedChannels...)
	if err != nil {
		ctx.ServerError("GenerateDrupalXML", err)
		return
	}

	ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
	ctx.Resp.WriteHeader(http.StatusOK)
	_, _ = ctx.Resp.Write(xmlData)
}

// ServeWHMCSJSON generates and serves a WHMCS module update JSON.
func ServeWHMCSJSON(ctx *context.Context) {
	platform := ctx.Data["RepoUpdatePlatform"]
	if platform != "whmcs" {
		ctx.NotFound(nil)
		return
	}

	_, ok, _ := validateUpdateKey(ctx)
	if !ok {
		ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
		ctx.Resp.WriteHeader(http.StatusOK)
		_, _ = ctx.Resp.Write([]byte(`{"name":"","version":"0.0.0"}`))
		return
	}

	licenseKey := ctx.FormString("key")
	if licenseKey == "" {
		licenseKey = ctx.FormString("dlid")
	}

	data, err := updateserver.GenerateWHMCSJSON(ctx, ctx.Repo.Repository, licenseKey)
	if err != nil {
		ctx.ServerError("GenerateWHMCSJSON", err)
		return
	}

	jsonData, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		ctx.ServerError("json.Marshal", err)
		return
	}

	ctx.Resp.Header().Set("Content-Type", "application/json; charset=utf-8")
	ctx.Resp.WriteHeader(http.StatusOK)
	_, _ = ctx.Resp.Write(jsonData)
}
