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

package repo

import (
	"context"
	"strings"

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)

func init() {
	db.RegisterModel(new(RepoMetadata))
}

// RepoMetadata stores mokoplatform metadata settings for a repository.
// These fields correspond to the .mokogitea/manifest.xml schema and are
// exposed via API for use by Actions workflows and the mokoplatform CLI.
type RepoMetadata struct {
	ID        int64              `xorm:"pk autoincr"`
	RepoID    int64              `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`

	// identity section
	Name        string `xorm:"TEXT 'name'"`         // project name
	Org         string `xorm:"TEXT 'org'"`           // organization name
	Description string `xorm:"TEXT 'description'"`   // project description
	LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later"
	LicenseName string `xorm:"TEXT 'license_name'"`  // human-readable license name

	// governance section
	Platform         string `xorm:"VARCHAR(50) 'platform'"`          // go, php, node, python, etc.
	StandardsVersion string `xorm:"VARCHAR(20) 'standards_version'"` // mokoplatform standards version
	StandardsSource  string `xorm:"TEXT 'standards_source'"`          // URL to standards repo

	// versioning
	VersionPrefix string `xorm:"TEXT 'version_prefix'"`  // tag prefix stripped for version display, e.g. "v1.26.1-moko."
	ElementName   string `xorm:"TEXT 'element_name'"`    // full element name override, e.g. "pkg_mokowaas" (auto-constructed if empty)

	// distribution metadata (used by update server feed generation)
	Maintainer    string `xorm:"TEXT 'maintainer'"`      // maintainer/author name
	MaintainerURL string `xorm:"TEXT 'maintainer_url'"`  // maintainer website
	InfoURL       string `xorm:"TEXT 'info_url'"`        // extension info/product page URL
	TargetVersion string `xorm:"TEXT 'target_version'"`  // target platform version regex, e.g. "(5|6)\..*"
	PHPMinimum    string `xorm:"VARCHAR(20) 'php_minimum'"` // minimum PHP version, e.g. "8.1"

	// build section
	Language    string `xorm:"VARCHAR(50) 'language'"`     // Go, PHP, TypeScript, etc.
	ExtensionType string `xorm:"VARCHAR(50) 'extension_type'"` // component, module, plugin, package, template, library, file
	EntryPoint  string `xorm:"TEXT 'entry_point'"`          // build entry point path

	CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
	UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}

func (RepoMetadata) TableName() string {
	return "repo_manifest"
}

// joomlaTypePrefix maps Joomla extension types to their element name prefixes.
// Derived from Joomla 6 source: libraries/src/Installer/Adapter/*Adapter.php
// Plugins use bare names (no prefix) — the folder column determines the plugin group.
var joomlaTypePrefix = map[string]string{
	"component": "com_",
	"module":    "mod_",
	"package":   "pkg_",
	"template":  "tpl_",
	"library":   "lib_",
	"file":      "file_",
}

// cleanJoomlaElement replicates Joomla's InputFilter::clean($value, 'cmd')
// which lowercases and strips all characters except [a-z0-9._-].
func cleanJoomlaElement(name string) string {
	lower := strings.ToLower(name)
	var b strings.Builder
	b.Grow(len(lower))
	for _, r := range lower {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' {
			b.WriteRune(r)
		}
	}
	return b.String()
}

// AutoElementName returns the auto-constructed Joomla element name (e.g. pkg_mokosuitebackup).
// The name is lowercased and cleaned to match Joomla's InputFilter::clean('cmd') behavior.
func (m *RepoMetadata) AutoElementName() string {
	if m.Name == "" || m.ExtensionType == "" {
		return ""
	}
	cleaned := cleanJoomlaElement(m.Name)
	if prefix, ok := joomlaTypePrefix[m.ExtensionType]; ok {
		return prefix + cleaned
	}
	return cleaned
}

// FullElementName returns the effective element name: override if set, otherwise auto-constructed.
func (m *RepoMetadata) FullElementName() string {
	if m.ElementName != "" {
		return m.ElementName
	}
	return m.AutoElementName()
}

// ElementNameMismatch returns true if an override is set that differs from the auto-constructed name.
func (m *RepoMetadata) ElementNameMismatch() bool {
	if m.ElementName == "" {
		return false
	}
	auto := m.AutoElementName()
	return auto != "" && m.ElementName != auto
}

// DerivedDisplayName computes the display name from ExtensionType and Name.
// Format: "Package - MokoSuiteBackup" (titlecased type + repo name).
// Falls back to just the Name if ExtensionType is empty.
func (m *RepoMetadata) DerivedDisplayName() string {
	if m.Name == "" {
		return ""
	}
	if m.ExtensionType == "" {
		return m.Name
	}
	title := strings.ToUpper(m.ExtensionType[:1]) + m.ExtensionType[1:]
	return title + " - " + m.Name
}

// GetRepoMetadata returns the metadata for a repo, or nil if none exists.
func GetRepoMetadata(ctx context.Context, repoID int64) (*RepoMetadata, error) {
	m := new(RepoMetadata)
	has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(m)
	if err != nil {
		return nil, err
	}
	if !has {
		return nil, nil
	}
	return m, nil
}

// CreateOrUpdateRepoMetadata upserts a repo metadata.
func CreateOrUpdateRepoMetadata(ctx context.Context, m *RepoMetadata) error {
	existing := new(RepoMetadata)
	has, err := db.GetEngine(ctx).Where("repo_id = ?", m.RepoID).Get(existing)
	if err != nil {
		return err
	}
	if has {
		m.ID = existing.ID
		_, err = db.GetEngine(ctx).ID(m.ID).AllCols().Update(m)
		return err
	}
	_, err = db.GetEngine(ctx).Insert(m)
	return err
}

// DeleteRepoMetadata deletes the metadata for a repo.
func DeleteRepoMetadata(ctx context.Context, repoID int64) error {
	_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(RepoMetadata))
	return err
}
