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

package repo

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

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
	updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
	repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)

// Joomla changelog XML structures.
type xmlChangelogs struct {
	XMLName    xml.Name         `xml:"changelogs"`
	Changelogs []xmlChangelog   `xml:"changelog"`
}

type xmlChangelog struct {
	Element   string       `xml:"element"`
	Type      string       `xml:"type"`
	Version   string       `xml:"version"`
	Security  *xmlItems    `xml:"security,omitempty"`
	Fix       *xmlItems    `xml:"fix,omitempty"`
	Addition  *xmlItems    `xml:"addition,omitempty"`
	Change    *xmlItems    `xml:"change,omitempty"`
	Remove    *xmlItems    `xml:"remove,omitempty"`
	Note      *xmlItems    `xml:"note,omitempty"`
}

type xmlItems struct {
	Items []string `xml:"item"`
}

// ServeChangelogXML generates Joomla-compatible changelog.xml from release notes.
func ServeChangelogXML(ctx *context.Context) {
	repo := ctx.Repo.Repository

	releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
		RepoID:        repo.ID,
		ListOptions:   db.ListOptionsAll,
		IncludeDrafts: false,
		IncludeTags:   false,
	})
	if err != nil {
		ctx.ServerError("FindReleases", err)
		return
	}

	if err := repo.LoadOwner(ctx); err != nil {
		ctx.ServerError("LoadOwner", err)
		return
	}

	// Get extension metadata for element name and type.
	cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
	element := strings.ToLower(repo.Name)
	extType := "component"
	if cfg != nil {
		if cfg.ExtensionName != "" {
			element = cfg.ExtensionName
		}
		if cfg.ExtensionType != "" {
			extType = cfg.ExtensionType
		}
	}

	var changelogs xmlChangelogs
	for _, rel := range releases {
		if rel.IsDraft || rel.IsTag || rel.Note == "" {
			continue
		}

		version := extractVersionFromTag(rel.TagName)
		// If the tag is a stream name, try the release title for the version.
		if version == rel.TagName && (version == "stable" || version == "release-candidate" || version == "beta" || version == "alpha" || version == "development") {
			if titleVer := extractVersionFromTag(rel.Title); titleVer != "" {
				version = titleVer
			}
		}
		cl := xmlChangelog{
			Element: element,
			Type:    extType,
			Version: version,
		}

		// Parse release notes into categories.
		security, fixes, additions, changes, removals, notes := parseReleaseNotes(rel.Note)
		if len(security) > 0 {
			cl.Security = &xmlItems{Items: security}
		}
		if len(fixes) > 0 {
			cl.Fix = &xmlItems{Items: fixes}
		}
		if len(additions) > 0 {
			cl.Addition = &xmlItems{Items: additions}
		}
		if len(changes) > 0 {
			cl.Change = &xmlItems{Items: changes}
		}
		if len(removals) > 0 {
			cl.Remove = &xmlItems{Items: removals}
		}
		if len(notes) > 0 {
			cl.Note = &xmlItems{Items: notes}
		}

		// If no categorized items, put the whole note as a single note item.
		if cl.Security == nil && cl.Fix == nil && cl.Addition == nil &&
			cl.Change == nil && cl.Remove == nil && cl.Note == nil {
			cl.Note = &xmlItems{Items: []string{truncate(rel.Note, 200)}}
		}

		changelogs.Changelogs = append(changelogs.Changelogs, cl)
	}

	output, err := xml.MarshalIndent(changelogs, "", "  ")
	if err != nil {
		ctx.ServerError("xml.MarshalIndent", err)
		return
	}

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

// parseReleaseNotes extracts categorized items from Keep-a-Changelog style markdown.
// Supports sections: ### Added, ### Changed, ### Fixed, ### Security, ### Removed, ### Deprecated
func parseReleaseNotes(note string) (security, fixes, additions, changes, removals, notes []string) {
	lines := strings.Split(note, "\n")
	var currentCategory *[]string

	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		lower := strings.ToLower(trimmed)

		// Detect section headers.
		if strings.HasPrefix(lower, "### ") || strings.HasPrefix(lower, "## ") {
			section := strings.TrimLeft(lower, "# ")
			switch {
			case strings.Contains(section, "security"):
				currentCategory = &security
			case strings.Contains(section, "fix"), strings.Contains(section, "bug"):
				currentCategory = &fixes
			case strings.Contains(section, "add"), strings.Contains(section, "new"), strings.Contains(section, "feature"):
				currentCategory = &additions
			case strings.Contains(section, "change"), strings.Contains(section, "update"), strings.Contains(section, "improve"):
				currentCategory = &changes
			case strings.Contains(section, "remov"), strings.Contains(section, "delet"):
				currentCategory = &removals
			case strings.Contains(section, "deprecat"), strings.Contains(section, "note"):
				currentCategory = &notes
			default:
				currentCategory = &notes
			}
			continue
		}

		// Parse list items (- item or * item).
		if (strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ")) && len(trimmed) > 2 {
			item := strings.TrimLeft(trimmed[2:], " ")
			if currentCategory != nil {
				*currentCategory = append(*currentCategory, item)
			} else {
				notes = append(notes, item)
			}
		}
	}

	return
}

// extractVersionFromTag strips common prefixes to get a clean version string.
func extractVersionFromTag(tagName string) string {
	v := tagName
	v = strings.TrimPrefix(v, "v")
	v = strings.TrimPrefix(v, "release-")
	v = strings.TrimPrefix(v, "release/")
	return v
}

func truncate(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen] + "..."
}

// formatChangelogURL returns the changelog.xml URL for use in updates.xml.
func formatChangelogURL(repoLink string) string {
	return fmt.Sprintf("%s/changelog.xml", repoLink)
}
