package gobackend

import (
	"encoding/json"
	"fmt"
	"net/url"
	"regexp"
	"strings"
)

var extensionIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,127}$`)

type ExtensionType string

const (
	ExtensionTypeMetadataProvider ExtensionType = "metadata_provider"
	ExtensionTypeDownloadProvider ExtensionType = "download_provider"
	ExtensionTypeLyricsProvider   ExtensionType = "lyrics_provider"
)

type SettingType string

const (
	SettingTypeString SettingType = "string"
	SettingTypeNumber SettingType = "number"
	SettingTypeBool   SettingType = "boolean"
	SettingTypeSelect SettingType = "select"
	SettingTypeButton SettingType = "button" // Action button that calls a JS function
)

type ExtensionPermissions struct {
	Network   []string `json:"network"`
	Storage   bool     `json:"storage"`
	File      bool     `json:"file"`
	AllowHTTP bool     `json:"allowHttp,omitempty"`
}

type ExtensionSetting struct {
	Key         string      `json:"key"`
	Type        SettingType `json:"type"`
	Label       string      `json:"label"`
	Description string      `json:"description,omitempty"`
	Required    bool        `json:"required,omitempty"`
	Secret      bool        `json:"secret,omitempty"`
	Default     any         `json:"default,omitempty"`
	Options     []string    `json:"options,omitempty"`
	Action      string      `json:"action,omitempty"`
}

type QualityOption struct {
	ID          string                   `json:"id"`
	Label       string                   `json:"label"`
	Description string                   `json:"description"`
	Settings    []QualitySpecificSetting `json:"settings,omitempty"`
}

type QualitySpecificSetting struct {
	Key         string      `json:"key"`
	Type        SettingType `json:"type"`
	Label       string      `json:"label"`
	Description string      `json:"description,omitempty"`
	Required    bool        `json:"required,omitempty"`
	Secret      bool        `json:"secret,omitempty"`
	Default     any         `json:"default,omitempty"`
	Options     []string    `json:"options,omitempty"`
}

type SearchFilter struct {
	ID    string `json:"id"`
	Label string `json:"label,omitempty"`
	Icon  string `json:"icon,omitempty"`
}

type SearchBehaviorConfig struct {
	Enabled         bool           `json:"enabled"`
	Placeholder     string         `json:"placeholder,omitempty"`
	Primary         bool           `json:"primary,omitempty"`
	Icon            string         `json:"icon,omitempty"`
	ThumbnailRatio  string         `json:"thumbnailRatio,omitempty"`
	ThumbnailWidth  int            `json:"thumbnailWidth,omitempty"`
	ThumbnailHeight int            `json:"thumbnailHeight,omitempty"`
	Filters         []SearchFilter `json:"filters,omitempty"`
}

type URLHandlerConfig struct {
	Enabled  bool     `json:"enabled"`
	Patterns []string `json:"patterns,omitempty"`
}

type TrackMatchingConfig struct {
	CustomMatching    bool   `json:"customMatching"`
	Strategy          string `json:"strategy,omitempty"`
	DurationTolerance int    `json:"durationTolerance,omitempty"`
}

type PostProcessingHook struct {
	ID               string   `json:"id"`
	Name             string   `json:"name"`
	Description      string   `json:"description,omitempty"`
	DefaultEnabled   bool     `json:"defaultEnabled,omitempty"`
	SupportedFormats []string `json:"supportedFormats,omitempty"`
}

type PostProcessingConfig struct {
	Enabled bool                 `json:"enabled"`
	Hooks   []PostProcessingHook `json:"hooks,omitempty"`
}

type ExtensionHealthCheck struct {
	ID              string `json:"id"`
	Label           string `json:"label,omitempty"`
	URL             string `json:"url"`
	Method          string `json:"method,omitempty"`
	ServiceKey      string `json:"serviceKey,omitempty"`
	TimeoutMs       int    `json:"timeoutMs,omitempty"`
	CacheTTLSeconds int    `json:"cacheTtlSeconds,omitempty"`
	Required        bool   `json:"required,omitempty"`
}

type SignedSessionEndpoints struct {
	Bootstrap string `json:"bootstrap,omitempty"`
	Challenge string `json:"challenge,omitempty"`
	Exchange  string `json:"exchange,omitempty"`
	Refresh   string `json:"refresh,omitempty"`
}

type SignedSessionConfig struct {
	Namespace         string                 `json:"namespace"`
	BaseURL           string                 `json:"baseUrl"`
	AppVersion        string                 `json:"appVersion,omitempty"`
	Platform          string                 `json:"platform,omitempty"`
	CallbackURL       string                 `json:"callbackUrl,omitempty"`
	SchemeLabel       string                 `json:"schemeLabel,omitempty"`
	HeaderPrefix      string                 `json:"headerPrefix,omitempty"`
	TimeWindowSeconds int                    `json:"timeWindowSeconds,omitempty"`
	Endpoints         SignedSessionEndpoints `json:"endpoints,omitempty"`
}

type ExtensionManifest struct {
	Name                    string                 `json:"name"`
	DisplayName             string                 `json:"displayName"`
	Version                 string                 `json:"version"`
	Description             string                 `json:"description"`
	Homepage                string                 `json:"homepage,omitempty"`
	Icon                    string                 `json:"icon,omitempty"`
	Types                   []ExtensionType        `json:"type"`
	Permissions             ExtensionPermissions   `json:"permissions"`
	Settings                []ExtensionSetting     `json:"settings,omitempty"`
	QualityOptions          []QualityOption        `json:"qualityOptions,omitempty"`
	MinAppVersion           string                 `json:"minAppVersion,omitempty"`
	SkipMetadataEnrichment  bool                   `json:"skipMetadataEnrichment,omitempty"`
	SkipLyrics              bool                   `json:"skipLyrics,omitempty"`
	StopProviderFallback    bool                   `json:"stopProviderFallback,omitempty"`
	SkipBuiltInFallback     bool                   `json:"skipBuiltInFallback,omitempty"`
	SearchBehavior          *SearchBehaviorConfig  `json:"searchBehavior,omitempty"`
	URLHandler              *URLHandlerConfig      `json:"urlHandler,omitempty"`
	TrackMatching           *TrackMatchingConfig   `json:"trackMatching,omitempty"`
	PostProcessing          *PostProcessingConfig  `json:"postProcessing,omitempty"`
	ServiceHealth           []ExtensionHealthCheck `json:"serviceHealth,omitempty"`
	SignedSession           *SignedSessionConfig   `json:"signedSession,omitempty"`
	RequiredRuntimeFeatures []string               `json:"requiredRuntimeFeatures,omitempty"`
	Capabilities            map[string]any         `json:"capabilities,omitempty"`
}

type ManifestValidationError struct {
	Field   string
	Message string
}

func (e *ManifestValidationError) Error() string {
	return fmt.Sprintf("manifest validation error: %s - %s", e.Field, e.Message)
}

func ParseManifest(data []byte) (*ExtensionManifest, error) {
	var manifest ExtensionManifest
	if err := json.Unmarshal(data, &manifest); err != nil {
		return nil, fmt.Errorf("failed to parse manifest JSON: %w", err)
	}

	if err := manifest.Validate(); err != nil {
		return nil, err
	}

	return &manifest, nil
}

func (m *ExtensionManifest) Validate() error {
	if strings.TrimSpace(m.Name) == "" {
		return &ManifestValidationError{Field: "name", Message: "name is required"}
	}
	if !extensionIDPattern.MatchString(m.Name) {
		return &ManifestValidationError{
			Field:   "name",
			Message: "name must be a lowercase extension ID containing only letters, numbers, '.', '_' or '-'",
		}
	}

	if strings.TrimSpace(m.Version) == "" {
		return &ManifestValidationError{Field: "version", Message: "version is required"}
	}

	if strings.TrimSpace(m.Description) == "" {
		return &ManifestValidationError{Field: "description", Message: "description is required"}
	}

	if len(m.Types) == 0 {
		return &ManifestValidationError{Field: "type", Message: "at least one type is required"}
	}

	for _, t := range m.Types {
		if t != ExtensionTypeMetadataProvider && t != ExtensionTypeDownloadProvider && t != ExtensionTypeLyricsProvider {
			return &ManifestValidationError{
				Field:   "type",
				Message: fmt.Sprintf("invalid extension type: %s (must be 'metadata_provider', 'download_provider', or 'lyrics_provider')", t),
			}
		}
	}

	for i, setting := range m.Settings {
		if strings.TrimSpace(setting.Key) == "" {
			return &ManifestValidationError{
				Field:   fmt.Sprintf("settings[%d].key", i),
				Message: "setting key is required",
			}
		}

		if setting.Type == "" {
			return &ManifestValidationError{
				Field:   fmt.Sprintf("settings[%d].type", i),
				Message: "setting type is required",
			}
		}

		if setting.Type == SettingTypeSelect && len(setting.Options) == 0 {
			return &ManifestValidationError{
				Field:   fmt.Sprintf("settings[%d].options", i),
				Message: "select type requires options",
			}
		}

		if setting.Type == SettingTypeButton && setting.Action == "" {
			return &ManifestValidationError{
				Field:   fmt.Sprintf("settings[%d].action", i),
				Message: "button type requires action (JS function name)",
			}
		}
	}

	for i, check := range m.ServiceHealth {
		if strings.TrimSpace(check.ID) == "" {
			return &ManifestValidationError{
				Field:   fmt.Sprintf("serviceHealth[%d].id", i),
				Message: "health check id is required",
			}
		}
		if strings.TrimSpace(check.URL) == "" {
			return &ManifestValidationError{
				Field:   fmt.Sprintf("serviceHealth[%d].url", i),
				Message: "health check url is required",
			}
		}
		method := strings.ToUpper(strings.TrimSpace(check.Method))
		if method != "" && method != "GET" && method != "HEAD" {
			return &ManifestValidationError{
				Field:   fmt.Sprintf("serviceHealth[%d].method", i),
				Message: "health check method must be GET or HEAD",
			}
		}
	}

	if m.SignedSession != nil {
		if !m.Permissions.Storage {
			return &ManifestValidationError{Field: "permissions.storage", Message: "signedSession requires storage permission"}
		}
		if strings.TrimSpace(m.SignedSession.Namespace) == "" {
			return &ManifestValidationError{Field: "signedSession.namespace", Message: "namespace is required"}
		}
		baseURL := strings.TrimSpace(m.SignedSession.BaseURL)
		if baseURL == "" {
			return &ManifestValidationError{Field: "signedSession.baseUrl", Message: "baseUrl is required"}
		}
		if !strings.HasPrefix(strings.ToLower(baseURL), "https://") {
			return &ManifestValidationError{Field: "signedSession.baseUrl", Message: "baseUrl must use https"}
		}
		parsed, err := url.Parse(baseURL)
		if err != nil || parsed.Hostname() == "" {
			return &ManifestValidationError{Field: "signedSession.baseUrl", Message: "baseUrl is invalid"}
		}
		if !m.IsDomainAllowed(parsed.Hostname()) {
			return &ManifestValidationError{Field: "signedSession.baseUrl", Message: "baseUrl host must be listed in permissions.network"}
		}
	}
	if m.HasCapability("rawFfmpeg") && !m.Permissions.File {
		return &ManifestValidationError{Field: "permissions.file", Message: "rawFfmpeg capability requires file permission"}
	}
	if err := validateDownloadTransferCapability(m.Capabilities); err != nil {
		return &ManifestValidationError{Field: "capabilities.downloadTransfer", Message: err.Error()}
	}

	return nil
}

func (m *ExtensionManifest) HasCapability(name string) bool {
	if m == nil || m.Capabilities == nil {
		return false
	}
	value, ok := m.Capabilities[name]
	if !ok {
		return false
	}
	enabled, ok := value.(bool)
	return ok && enabled
}

func (m *ExtensionManifest) HasType(t ExtensionType) bool {
	for _, et := range m.Types {
		if et == t {
			return true
		}
	}
	return false
}

func (m *ExtensionManifest) IsMetadataProvider() bool {
	return m.HasType(ExtensionTypeMetadataProvider)
}

func (m *ExtensionManifest) IsDownloadProvider() bool {
	return m.HasType(ExtensionTypeDownloadProvider)
}

func (m *ExtensionManifest) IsLyricsProvider() bool {
	return m.HasType(ExtensionTypeLyricsProvider)
}

func (m *ExtensionManifest) StopsProviderFallback() bool {
	if m == nil {
		return false
	}
	return m.StopProviderFallback || m.SkipBuiltInFallback
}

func (m *ExtensionManifest) IsDomainAllowed(domain string) bool {
	domain = strings.ToLower(strings.TrimSpace(domain))
	for _, allowed := range m.Permissions.Network {
		allowed = strings.ToLower(strings.TrimSpace(allowed))
		if allowed == domain {
			return true
		}
		// Support wildcard subdomains (e.g., *.example.com)
		if strings.HasPrefix(allowed, "*.") {
			suffix := allowed[1:]
			if strings.HasSuffix(domain, suffix) {
				return true
			}
		}
	}
	return false
}

func (m *ExtensionManifest) HasCustomSearch() bool {
	return m.SearchBehavior != nil && m.SearchBehavior.Enabled
}

func (m *ExtensionManifest) HasCustomMatching() bool {
	return m.TrackMatching != nil && m.TrackMatching.CustomMatching
}

func (m *ExtensionManifest) HasPostProcessing() bool {
	return m.PostProcessing != nil && m.PostProcessing.Enabled
}

func (m *ExtensionManifest) HasURLHandler() bool {
	return m.URLHandler != nil && m.URLHandler.Enabled && len(m.URLHandler.Patterns) > 0
}

// MatchesURL reports whether one of the handler's patterns matches the URL.
// Web patterns are anchored to the URL's host (exact domain or subdomain,
// optional path prefix) — never matched as a raw substring, so "spotify.com"
// cannot match a URL that merely embeds it in a query parameter. Patterns
// ending in ":" (e.g. "spotify:") match custom URI schemes as prefixes.
func (m *ExtensionManifest) MatchesURL(urlStr string) bool {
	if !m.HasURLHandler() {
		return false
	}

	urlStr = strings.ToLower(strings.TrimSpace(urlStr))
	parsed, parseErr := url.Parse(urlStr)

	for _, pattern := range m.URLHandler.Patterns {
		pattern = strings.ToLower(strings.TrimSpace(pattern))
		if pattern == "" {
			continue
		}

		// Scheme patterns anchor to the front of the URI.
		if !strings.Contains(pattern, "/") && strings.HasSuffix(pattern, ":") {
			if strings.HasPrefix(urlStr, pattern) {
				return true
			}
			continue
		}

		if parseErr != nil || parsed.Host == "" {
			continue
		}
		host := parsed.Hostname()
		urlPath := parsed.Path
		if urlPath == "" {
			urlPath = "/"
		}

		if idx := strings.Index(pattern, "://"); idx >= 0 {
			pattern = pattern[idx+3:]
		}
		patternHost, patternPath, hasPath := strings.Cut(pattern, "/")
		if patternHost == "" {
			continue
		}
		if host != patternHost && !strings.HasSuffix(host, "."+patternHost) {
			continue
		}
		if hasPath && patternPath != "" &&
			!strings.HasPrefix(urlPath, "/"+patternPath) {
			continue
		}
		return true
	}
	return false
}

func (m *ExtensionManifest) GetPostProcessingHooks() []PostProcessingHook {
	if m.PostProcessing == nil {
		return nil
	}
	return m.PostProcessing.Hooks
}

func (m *ExtensionManifest) ToJSON() ([]byte, error) {
	return json.Marshal(m)
}
