package proxy

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"sync"

	"github.com/alexei/docker-watcher/internal/dns"
	"github.com/alexei/docker-watcher/internal/npm"
	"github.com/alexei/docker-watcher/internal/store"
)

// Manager handles the lifecycle of standalone proxy hosts.
type Manager struct {
	store *store.Store
	npm   *npm.Client
	dnsMu sync.RWMutex
	dns   dns.Provider // nil when wildcard DNS is active
}

// NewManager creates a new proxy manager.
func NewManager(st *store.Store, npmClient *npm.Client) *Manager {
	return &Manager{
		store: st,
		npm:   npmClient,
	}
}

// SetDNSProvider sets the DNS provider for managing DNS records.
func (m *Manager) SetDNSProvider(provider dns.Provider) {
	m.dnsMu.Lock()
	defer m.dnsMu.Unlock()
	m.dns = provider
}

// getDNS returns the current DNS provider under read lock.
func (m *Manager) getDNS() dns.Provider {
	m.dnsMu.RLock()
	defer m.dnsMu.RUnlock()
	return m.dns
}

// CreateProxyRequest is the input for creating a standalone proxy.
type CreateProxyRequest struct {
	Domain          string `json:"domain"`
	DestinationURL  string `json:"destination_url"`
	DestinationPort int    `json:"destination_port"`
}

// UpdateProxyRequest is the input for updating a standalone proxy.
type UpdateProxyRequest struct {
	Domain          string `json:"domain"`
	DestinationURL  string `json:"destination_url"`
	DestinationPort int    `json:"destination_port"`
}

// ProxyView is a unified view of both standalone and deploy-managed proxies.
type ProxyView struct {
	ID           string `json:"id"`
	Domain       string `json:"domain"`
	Destination  string `json:"destination"`
	Type         string `json:"type"` // "standalone" or "managed"
	ProjectName  string `json:"project_name,omitempty"`
	StageName    string `json:"stage_name,omitempty"`
	HealthStatus string `json:"health_status"`
	SSLEnabled   bool   `json:"ssl_enabled"`
	NpmProxyID   int    `json:"npm_proxy_id"`
	CreatedAt    string `json:"created_at"`
}

// CreateProxy validates the destination, creates an NPM proxy host, and saves to the store.
func (m *Manager) CreateProxy(ctx context.Context, req CreateProxyRequest) (store.StandaloneProxy, error) {
	// Validate destination.
	result := ValidateDestination(ctx, req.DestinationURL, req.DestinationPort)
	if !result.Valid {
		return store.StandaloneProxy{}, fmt.Errorf("destination validation failed: %s", lastFailedStep(result))
	}

	// Load settings for SSL certificate and domain.
	settings, err := m.store.GetSettings()
	if err != nil {
		return store.StandaloneProxy{}, fmt.Errorf("get settings: %w", err)
	}

	// Build NPM proxy host config.
	config := npm.ProxyHostConfig{
		DomainNames:    []string{req.Domain},
		ForwardScheme:  "http",
		ForwardHost:    req.DestinationURL,
		ForwardPort:    req.DestinationPort,
		CertificateID:  settings.SSLCertificateID,
		SSLForced:      settings.SSLCertificateID > 0,
		BlockExploits:  true,
		AllowWebsocket: true,
		HTTP2Support:   true,
		HSTSEnabled:    settings.SSLCertificateID > 0,
		Locations:      []any{},
	}

	// Create NPM proxy host.
	npmHost, err := m.npm.CreateProxyHost(ctx, config)
	if err != nil {
		return store.StandaloneProxy{}, fmt.Errorf("create NPM proxy host: %w", err)
	}

	slog.Info("created NPM proxy host for standalone proxy",
		"domain", req.Domain, "npm_proxy_id", npmHost.ID)

	// Save to store.
	proxy, err := m.store.CreateStandaloneProxy(store.StandaloneProxy{
		Domain:           req.Domain,
		DestinationURL:   req.DestinationURL,
		DestinationPort:  req.DestinationPort,
		SSLCertificateID: settings.SSLCertificateID,
		NpmProxyID:       npmHost.ID,
		HealthStatus:     "unknown",
	})
	if err != nil {
		// Best effort: clean up the NPM host if store insert fails.
		if delErr := m.npm.DeleteProxyHost(ctx, npmHost.ID); delErr != nil {
			slog.Error("failed to clean up NPM proxy host after store error",
				"npm_proxy_id", npmHost.ID, "error", delErr)
		}
		return store.StandaloneProxy{}, fmt.Errorf("save standalone proxy: %w", err)
	}

	// Create DNS record after successful store save.
	m.ensureDNS(ctx, req.Domain, proxy.ID)

	return proxy, nil
}

// UpdateProxy re-validates the destination, updates the NPM proxy host, and updates the store.
func (m *Manager) UpdateProxy(ctx context.Context, id string, req UpdateProxyRequest) (store.StandaloneProxy, error) {
	existing, err := m.store.GetStandaloneProxy(id)
	if err != nil {
		return store.StandaloneProxy{}, fmt.Errorf("get proxy: %w", err)
	}

	// Validate new destination.
	result := ValidateDestination(ctx, req.DestinationURL, req.DestinationPort)
	if !result.Valid {
		return store.StandaloneProxy{}, fmt.Errorf("destination validation failed: %s", lastFailedStep(result))
	}

	// Load settings for SSL certificate.
	settings, err := m.store.GetSettings()
	if err != nil {
		return store.StandaloneProxy{}, fmt.Errorf("get settings: %w", err)
	}

	// Update NPM proxy host.
	config := npm.ProxyHostConfig{
		DomainNames:    []string{req.Domain},
		ForwardScheme:  "http",
		ForwardHost:    req.DestinationURL,
		ForwardPort:    req.DestinationPort,
		CertificateID:  settings.SSLCertificateID,
		SSLForced:      settings.SSLCertificateID > 0,
		BlockExploits:  true,
		AllowWebsocket: true,
		HTTP2Support:   true,
		HSTSEnabled:    settings.SSLCertificateID > 0,
		Locations:      []any{},
	}

	if _, err := m.npm.UpdateProxyHost(ctx, existing.NpmProxyID, config); err != nil {
		return store.StandaloneProxy{}, fmt.Errorf("update NPM proxy host: %w", err)
	}

	// Update store.
	updated := existing
	updated.Domain = req.Domain
	updated.DestinationURL = req.DestinationURL
	updated.DestinationPort = req.DestinationPort
	updated.SSLCertificateID = settings.SSLCertificateID

	if err := m.store.UpdateStandaloneProxy(updated); err != nil {
		return store.StandaloneProxy{}, fmt.Errorf("update standalone proxy: %w", err)
	}

	// Update DNS records if domain changed.
	if existing.Domain != req.Domain {
		m.removeDNS(ctx, existing.Domain)
		m.ensureDNS(ctx, req.Domain, id)
	}

	// Re-read from store to get updated timestamps.
	return m.store.GetStandaloneProxy(id)
}

// DeleteProxy removes the NPM proxy host and deletes from the store.
func (m *Manager) DeleteProxy(ctx context.Context, id string) error {
	proxy, err := m.store.GetStandaloneProxy(id)
	if err != nil {
		return fmt.Errorf("get proxy: %w", err)
	}

	// Delete NPM proxy host.
	if proxy.NpmProxyID > 0 {
		if err := m.npm.DeleteProxyHost(ctx, proxy.NpmProxyID); err != nil {
			slog.Warn("failed to delete NPM proxy host (continuing with store deletion)",
				"npm_proxy_id", proxy.NpmProxyID, "error", err)
		}
	}

	// Remove DNS record.
	m.removeDNS(ctx, proxy.Domain)

	if err := m.store.DeleteStandaloneProxy(id); err != nil {
		return fmt.Errorf("delete standalone proxy: %w", err)
	}

	return nil
}

// GetProxy returns a single standalone proxy by ID.
func (m *Manager) GetProxy(id string) (store.StandaloneProxy, error) {
	proxy, err := m.store.GetStandaloneProxy(id)
	if err != nil {
		return store.StandaloneProxy{}, fmt.Errorf("get proxy: %w", err)
	}
	return proxy, nil
}

// ListProxies returns all standalone proxies.
func (m *Manager) ListProxies() ([]store.StandaloneProxy, error) {
	proxies, err := m.store.ListStandaloneProxies()
	if err != nil {
		return nil, fmt.Errorf("list proxies: %w", err)
	}
	return proxies, nil
}

// ListAllProxies returns a merged view of standalone and deploy-managed proxies.
func (m *Manager) ListAllProxies() ([]ProxyView, error) {
	views := []ProxyView{}

	// Standalone proxies.
	standalones, err := m.store.ListStandaloneProxies()
	if err != nil {
		return nil, fmt.Errorf("list standalone proxies: %w", err)
	}

	for _, p := range standalones {
		views = append(views, ProxyView{
			ID:           p.ID,
			Domain:       p.Domain,
			Destination:  fmt.Sprintf("%s:%d", p.DestinationURL, p.DestinationPort),
			Type:         "standalone",
			HealthStatus: p.HealthStatus,
			SSLEnabled:   p.SSLCertificateID > 0,
			NpmProxyID:   p.NpmProxyID,
			CreatedAt:    p.CreatedAt,
		})
	}

	// Deploy-managed proxies: instances with npm_proxy_id > 0.
	instances, err := m.store.ListAllInstances()
	if err != nil {
		return nil, fmt.Errorf("list instances: %w", err)
	}

	// Pre-load project and stage names to avoid N+1 queries.
	allProjects, _ := m.store.GetAllProjects()
	projectNames := make(map[string]string, len(allProjects))
	for _, p := range allProjects {
		projectNames[p.ID] = p.Name
	}
	stageNames := make(map[string]string)
	for _, p := range allProjects {
		stages, _ := m.store.GetStagesByProjectID(p.ID)
		for _, s := range stages {
			stageNames[s.ID] = s.Name
		}
	}

	for _, inst := range instances {
		if inst.NpmProxyID <= 0 {
			continue
		}

		projectName := projectNames[inst.ProjectID]
		if projectName == "" {
			projectName = inst.ProjectID
		}
		stageName := stageNames[inst.StageID]
		if stageName == "" {
			stageName = inst.StageID
		}

		cid := inst.ContainerID
		if len(cid) > 12 {
			cid = cid[:12]
		}
		destination := fmt.Sprintf("%s:%d", cid, inst.Port)
		if inst.Subdomain != "" {
			destination = fmt.Sprintf("%s:%d", inst.Subdomain, inst.Port)
		}

		healthStatus := "unknown"
		if inst.Status == "running" {
			healthStatus = "healthy"
		} else if inst.Status == "stopped" || inst.Status == "failed" {
			healthStatus = "unhealthy"
		}

		views = append(views, ProxyView{
			ID:           inst.ID,
			Domain:       inst.Subdomain,
			Destination:  destination,
			Type:         "managed",
			ProjectName:  projectName,
			StageName:    stageName,
			HealthStatus: healthStatus,
			SSLEnabled:   true, // managed proxies always get SSL from settings
			NpmProxyID:   inst.NpmProxyID,
			CreatedAt:    inst.CreatedAt,
		})
	}

	return views, nil
}

// ensureDNS creates or updates a DNS record for a standalone proxy domain. Best-effort.
func (m *Manager) ensureDNS(ctx context.Context, domain, proxyID string) {
	dnsProvider := m.getDNS()
	if dnsProvider == nil {
		return
	}
	settings, err := m.store.GetSettings()
	if err != nil {
		slog.Warn("dns: get settings for server IP", "error", err)
		return
	}
	if settings.ServerIP == "" {
		slog.Warn("dns: server IP not configured, skipping DNS record creation", "domain", domain)
		return
	}

	recordID, err := dnsProvider.EnsureRecord(ctx, domain, settings.ServerIP)
	if err != nil {
		slog.Warn("dns: failed to create/update record for standalone proxy", "domain", domain, "error", err)
		return
	}

	if _, err := m.store.CreateDNSRecord(store.DNSRecord{
		FQDN:             domain,
		ProviderRecordID: recordID,
		ConsumerType:     "standalone",
		ConsumerID:       proxyID,
	}); err != nil {
		// May already exist — try updating.
		if updateErr := m.store.UpdateDNSRecordProviderID(domain, recordID); updateErr != nil {
			slog.Warn("dns: failed to track record", "domain", domain, "error", updateErr)
		}
	}
	slog.Info("dns: record ensured for standalone proxy", "domain", domain)
}

// removeDNS deletes a DNS record for a standalone proxy domain. Best-effort.
func (m *Manager) removeDNS(ctx context.Context, domain string) {
	dnsProvider := m.getDNS()
	if dnsProvider == nil {
		return
	}
	if err := dnsProvider.DeleteRecord(ctx, domain); err != nil {
		slog.Warn("dns: failed to delete record for standalone proxy", "domain", domain, "error", err)
		return
	}
	if err := m.store.DeleteDNSRecord(domain); err != nil {
		slog.Warn("dns: failed to remove tracking record", "domain", domain, "error", err)
	}
	slog.Info("dns: record deleted for standalone proxy", "domain", domain)
}

// lastFailedStep returns the message of the last failed validation step.
func lastFailedStep(result ValidationResult) string {
	for _, step := range result.Steps {
		if !step.Passed {
			msg := step.Message
			if step.Hint != "" {
				msg += " — " + step.Hint
			}
			return msg
		}
	}
	return "unknown validation failure"
}

// IsNotFound checks if an error wraps store.ErrNotFound.
func IsNotFound(err error) bool {
	return errors.Is(err, store.ErrNotFound)
}
