package stacks

import (
	"log"
	"os"
	"path/filepath"
	"strings"

	"gopkg.in/yaml.v3"
)

// Metadata holds app information parsed from .felhom.yml.
type Metadata struct {
	DisplayName string `yaml:"display_name" json:"display_name"`
	Description string `yaml:"description" json:"description"`
	Category    string `yaml:"category" json:"category"`
	Subdomain   string `yaml:"subdomain" json:"subdomain"`
	Slug        string `yaml:"slug" json:"slug"`
	// OpenPath is appended to the app's public URL for the "Megnyitás" (open) link, for apps whose UI
	// isn't at "/" (e.g. Gokapi → "/admin"). Empty = bare root. Must start with "/".
	OpenPath       string                `yaml:"open_path,omitempty" json:"open_path,omitempty"`
	Resources      ResourceHints         `yaml:"resources" json:"resources"`
	DeployFields   []DeployField         `yaml:"deploy_fields" json:"deploy_fields"`
	AppInfo        AppInfo               `yaml:"app_info" json:"app_info"`
	OptionalConfig []OptionalConfigGroup `yaml:"optional_config" json:"optional_config"`
	HealthCheck    *HealthCheckConfig    `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"`
	Integrations   []IntegrationDef      `yaml:"integrations,omitempty" json:"integrations,omitempty"`
	// InitialCreds: for apps that auto-generate a first-login credential into a file inside the
	// container (e.g. Crafty's default-creds.txt). The controller reads + parses that file live and
	// surfaces it on the app page, so the customer never has to dig through logs. Optional.
	InitialCreds *InitialCredentials `yaml:"initial_credentials,omitempty" json:"initial_credentials,omitempty"`
	// SMTPMapping declares how this app's compose env receives the managed app-email relay settings.
	// Present only for apps that support outbound email; absent = the app has no email UI/injection.
	SMTPMapping *SMTPMapping `yaml:"smtp_mapping,omitempty" json:"smtp_mapping,omitempty"`
}

// SMTPMapping renames the generic relay settings (host / port / security / from / from-name)
// to an app's specific env-var keys, plus any fixed Extra vars (e.g. accept-invalid-cert
// flags). When app-email is on (global + per-app) the controller injects these at
// deploy/redeploy: host = the in-controller shim, port = 2525, security = SecurityValue
// (the app's term for STARTTLS), from = <FromLocal>@<allowlisted-domain>. The values are
// NEVER persisted to app.yaml — they are derived from settings on every compose, so a
// toggle change applies on the next redeploy without rewriting secrets. (Spike §7.)
type SMTPMapping struct {
	HostVar       string            `yaml:"host_var" json:"host_var"`             // env key for the shim host (required)
	PortVar       string            `yaml:"port_var" json:"port_var"`             // env key for the port (required)
	SecurityVar   string            `yaml:"security_var" json:"security_var"`     // env key for the TLS mode (optional)
	SecurityValue string            `yaml:"security_value" json:"security_value"` // app term for STARTTLS (e.g. "starttls", "TLS")
	FromVar       string            `yaml:"from_var" json:"from_var"`             // env key for the From address (required)
	FromNameVar   string            `yaml:"from_name_var" json:"from_name_var"`   // env key for the From display name (optional)
	FromLocal     string            `yaml:"from_local" json:"from_local"`         // From local-part (defaults to the app slug)
	Extra         map[string]string `yaml:"extra" json:"extra"`                   // fixed extra env (accept-invalid-cert flags, etc.)
	// TLSMode selects which shim listener the app is pointed at (which fixes the port):
	//   "" / "starttls" → :2525 (plaintext + STARTTLS advertised) — the default; existing apps unchanged.
	//   "plaintext"     → :2526 (plaintext, STARTTLS NOT advertised) — for clients that opportunistically
	//                     upgrade to STARTTLS and can't skip cert verification (cal.com, nextcloud).
	//   "implicit-tls"  → :2465 (whole connection TLS).
	TLSMode string `yaml:"tls_mode" json:"tls_mode"`
	// FromDomainVar: for apps that SPLIT the From into local-part + domain env vars (nextcloud:
	// MAIL_FROM_ADDRESS + MAIL_DOMAIN). When set, the controller injects FromVar=<local> and
	// FromDomainVar=<allowlisted-domain> separately instead of FromVar=<local>@<domain>.
	FromDomainVar string `yaml:"from_domain_var" json:"from_domain_var"`
}

// HasSMTPMapping reports whether this app declares a usable email mapping.
func (m *Metadata) HasSMTPMapping() bool {
	return m.SMTPMapping != nil && m.SMTPMapping.HostVar != "" && m.SMTPMapping.FromVar != ""
}

// InitialCredentials tells the controller how to extract an app's auto-generated first-login
// credential from a file inside the running container. The file path is catalog-defined (trusted).
// Verification stays catalog-driven so any future self-seeding app can reuse the mechanism.
type InitialCredentials struct {
	File   string `yaml:"file" json:"file"`     // path INSIDE the container
	Format string `yaml:"format" json:"format"` // "json" | "regex" | "plain"
	// Container overrides which container to read from; empty → the stack's main container.
	Container string `yaml:"container,omitempty" json:"container,omitempty"`
	// json format: which keys hold the username/password (password_key required; username optional).
	UsernameKey string `yaml:"username_key,omitempty" json:"username_key,omitempty"`
	PasswordKey string `yaml:"password_key,omitempty" json:"password_key,omitempty"`
	// regex format: patterns whose first capture group is the value (password_pattern required).
	UsernamePattern string `yaml:"username_pattern,omitempty" json:"username_pattern,omitempty"`
	PasswordPattern string `yaml:"password_pattern,omitempty" json:"password_pattern,omitempty"`
	// Note is shown alongside the credential (e.g. "initial password, change it after first login").
	Note string `yaml:"note,omitempty" json:"note,omitempty"`
}

// AppInfo holds detailed app information for the info page.
type AppInfo struct {
	Tagline       string   `yaml:"tagline" json:"tagline"`
	UseCases      []string `yaml:"use_cases" json:"use_cases"`
	FirstSteps    []string `yaml:"first_steps" json:"first_steps"`
	Prerequisites []string `yaml:"prerequisites" json:"prerequisites"`
	DefaultCreds  string   `yaml:"default_creds" json:"default_creds"`
	DocsURL       string   `yaml:"docs_url" json:"docs_url"`
}

// OptionalConfigGroup defines a group of optional config fields (e.g., "Metadata providers").
type OptionalConfigGroup struct {
	Group       string                `yaml:"group" json:"group"`
	Description string                `yaml:"description" json:"description"`
	Fields      []OptionalConfigField `yaml:"fields" json:"fields"`
}

// OptionalConfigField defines an individual optional config field.
type OptionalConfigField struct {
	EnvVar   string `yaml:"env_var" json:"env_var"`
	Label    string `yaml:"label" json:"label"`
	Type     string `yaml:"type" json:"type"`
	HelpURL  string `yaml:"help_url" json:"help_url"`
	HelpText string `yaml:"help_text" json:"help_text"`
}

// ResourceHints describe what the app needs.
type ResourceHints struct {
	MemRequest   string `yaml:"mem_request" json:"mem_request"`
	MemLimit     string `yaml:"mem_limit" json:"mem_limit"`
	PiCompatible bool   `yaml:"pi_compatible" json:"pi_compatible"`
	NeedsHDD     bool   `yaml:"needs_hdd" json:"needs_hdd"`
	HungarianUI  bool   `yaml:"hungarian_ui" json:"hungarian_ui"`
}

// DeployField defines one configuration field shown during first deployment.
type DeployField struct {
	EnvVar            string         `yaml:"env_var" json:"env_var"`
	Label             string         `yaml:"label" json:"label"`
	Type              string         `yaml:"type" json:"type"`         // domain, subdomain, secret, password, path, text, select, boolean
	Generate          string         `yaml:"generate" json:"generate"` // e.g., "password:24", "hex:32", "static:admin"
	Default           string         `yaml:"default" json:"default"`
	Required          bool           `yaml:"required" json:"required"`
	Placeholder       string         `yaml:"placeholder" json:"placeholder"`
	Description       string         `yaml:"description" json:"description"`
	LockedAfterDeploy bool           `yaml:"locked_after_deploy" json:"locked_after_deploy"`
	Options           []SelectOption `yaml:"options" json:"options,omitempty"`
	// DataKey marks a field as a DATA-ENCRYPTING key (e.g. AdventureLog's "Titkosítási kulcs"):
	// the app encrypts stored data with it, so regenerating it would render restored data
	// unreadable. It is a fail-closed annotation only — the recovery unit never stores secrets;
	// at restore the controller refuses (rather than silently restoring garbage) if a data_key
	// app's key cannot be recovered from the guest's app.yaml (live or via PBS). See Phase 2.
	DataKey bool `yaml:"data_key,omitempty" json:"data_key,omitempty"`
}

// DataKeyEnvVars returns the env-var names of fields marked data_key:true.
func (m *Metadata) DataKeyEnvVars() []string {
	var out []string
	for _, f := range m.DeployFields {
		if f.DataKey {
			out = append(out, f.EnvVar)
		}
	}
	return out
}

// SelectOption is a choice for "select" type fields.
type SelectOption struct {
	Value string `yaml:"value" json:"value"`
	Label string `yaml:"label" json:"label"`
}

// IntegrationDef defines a single integration this app can provide to a target app.
type IntegrationDef struct {
	Target      string `yaml:"target" json:"target"`           // target app slug: "filebrowser", "nextcloud"
	Label       string `yaml:"label" json:"label"`             // UI label (Hungarian)
	Description string `yaml:"description" json:"description"` // UI description
}

// HealthCheckConfig defines controller-side health probe configuration.
// When configured, the controller periodically probes the app's container
// and overrides the stack state to "unhealthy" if the service is not responding.
type HealthCheckConfig struct {
	Interval string            `yaml:"interval" json:"interval"` // e.g. "5m", "30s"; default "5m"
	Checks   []HealthCheckItem `yaml:"checks" json:"checks"`
}

// HealthCheckItem defines a single health check probe.
type HealthCheckItem struct {
	Type   string             `yaml:"type" json:"type"` // "http", "api", "tcp"
	Port   int                `yaml:"port" json:"port"`
	Path   string             `yaml:"path" json:"path"`                         // for http/api; default "/"
	Method string             `yaml:"method" json:"method"`                     // for api; default "GET"
	Expect *HealthCheckExpect `yaml:"expect,omitempty" json:"expect,omitempty"` // for api
}

// HealthCheckExpect defines expected response content for "api" type checks.
type HealthCheckExpect struct {
	Status       int    `yaml:"status" json:"status"`               // expected HTTP status code
	BodyContains string `yaml:"body_contains" json:"body_contains"` // string that must appear in response body
}

// LoadMetadata reads .felhom.yml from a stack directory.
// Returns default metadata if the file doesn't exist.
func LoadMetadata(stackDir string) Metadata {
	meta := Metadata{}

	path := filepath.Join(stackDir, ".felhom.yml")
	data, err := os.ReadFile(path)
	if err != nil {
		// No metadata file — build defaults from directory name
		dirName := filepath.Base(stackDir)
		meta.DisplayName = toTitleCase(strings.ReplaceAll(dirName, "-", " "))
		meta.Slug = dirName
		meta.Category = "tools"
		return meta
	}

	if err := yaml.Unmarshal(data, &meta); err != nil {
		log.Printf("[ERROR] [stacks] Failed to parse .felhom.yml in %s: %v", stackDir, err)
		dirName := filepath.Base(stackDir)
		meta.DisplayName = toTitleCase(strings.ReplaceAll(dirName, "-", " "))
		meta.Slug = dirName
		return meta
	}

	// Fill in defaults for missing fields
	dirName := filepath.Base(stackDir)
	if meta.Slug == "" {
		meta.Slug = dirName
	}
	if meta.DisplayName == "" {
		meta.DisplayName = toTitleCase(strings.ReplaceAll(dirName, "-", " "))
	}
	if meta.Category == "" {
		meta.Category = "tools"
	}

	// Default healthcheck fields
	if meta.HealthCheck != nil {
		if meta.HealthCheck.Interval == "" {
			meta.HealthCheck.Interval = "5m"
		}
		for i := range meta.HealthCheck.Checks {
			if meta.HealthCheck.Checks[i].Path == "" && (meta.HealthCheck.Checks[i].Type == "http" || meta.HealthCheck.Checks[i].Type == "api") {
				meta.HealthCheck.Checks[i].Path = "/"
			}
			if meta.HealthCheck.Checks[i].Method == "" && meta.HealthCheck.Checks[i].Type == "api" {
				meta.HealthCheck.Checks[i].Method = "GET"
			}
		}
	}

	// DOMAIN and SUBDOMAIN fields are always auto-filled/required — mark implicitly
	for i := range meta.DeployFields {
		if meta.DeployFields[i].Type == "domain" || meta.DeployFields[i].Type == "subdomain" {
			meta.DeployFields[i].Required = true
			meta.DeployFields[i].LockedAfterDeploy = true
		}
		// secret fields are always locked after deploy
		if meta.DeployFields[i].Type == "secret" {
			meta.DeployFields[i].LockedAfterDeploy = true
		}
	}

	return meta
}

// HasDeployFields returns true if the app has any user-facing deploy fields
// (i.e., fields beyond auto-filled domain and auto-generated secrets).
func (m *Metadata) HasDeployFields() bool {
	for _, f := range m.DeployFields {
		if f.Type != "domain" && f.Type != "secret" {
			return true
		}
	}
	return false
}

// UserFacingFields returns only fields the user needs to interact with.
// Excludes auto-filled (domain) and fully hidden (secret) fields.
func (m *Metadata) UserFacingFields() []DeployField {
	var fields []DeployField
	for _, f := range m.DeployFields {
		if f.Type != "domain" && f.Type != "secret" {
			fields = append(fields, f)
		}
	}
	return fields
}

// AutoGeneratedFields returns fields that are generated without user input.
func (m *Metadata) AutoGeneratedFields() []DeployField {
	var fields []DeployField
	for _, f := range m.DeployFields {
		if f.Type == "secret" || f.Type == "domain" {
			fields = append(fields, f)
		}
	}
	return fields
}

// HasAppInfo returns true if the metadata has any app info content.
func (m *Metadata) HasAppInfo() bool {
	return m.AppInfo.Tagline != "" || len(m.AppInfo.UseCases) > 0 || len(m.AppInfo.FirstSteps) > 0
}

// HasOptionalConfig returns true if the metadata has any optional config groups.
func (m *Metadata) HasOptionalConfig() bool {
	return len(m.OptionalConfig) > 0
}

// HasIntegrations returns true if the metadata defines any integrations.
func (m *Metadata) HasIntegrations() bool {
	return len(m.Integrations) > 0
}
