package origin

import (
	"net/url"
	"os"
	"strings"

	"github.com/rs/zerolog/log"
)

// Config captures the canonical origin-related runtime settings for local,
// test, and production request validation.
type Config struct {
	FrontendURL string
	APIBaseURL  string

	allowedOrigins       []string
	allowedOriginLookup  map[string]struct{}
	allowedRedirectHosts map[string]struct{}
}

// CanonicalOrigin returns a normalized origin of the form scheme://host[:port].
func CanonicalOrigin(raw string) (string, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return "", false
	}

	u, err := url.Parse(raw)
	if err != nil {
		return "", false
	}

	if u.Scheme != "http" && u.Scheme != "https" {
		return "", false
	}

	if u.Host == "" || u.RawQuery != "" || u.Fragment != "" {
		return "", false
	}

	if u.Path != "" && u.Path != "/" {
		return "", false
	}

	return u.Scheme + "://" + u.Host, true
}

// CanonicalOriginFromURL returns the browser origin for an absolute URL. Unlike
// CanonicalOrigin, this accepts paths and query strings because OAuth redirect
// URIs are registered as full callback URLs while CORS answers by origin.
func CanonicalOriginFromURL(raw string) (string, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return "", false
	}

	u, err := url.Parse(raw)
	if err != nil || !u.IsAbs() || u.Host == "" || u.User != nil || u.Fragment != "" {
		return "", false
	}

	switch u.Scheme {
	case "https":
	case "http":
		host := strings.ToLower(u.Hostname())
		if host != "localhost" && host != "127.0.0.1" && host != "::1" {
			return "", false
		}
	default:
		return "", false
	}

	return u.Scheme + "://" + u.Host, true
}

// LoadConfigFromEnv resolves the canonical origin contract from environment.
func LoadConfigFromEnv() Config {
	cfg := Config{
		FrontendURL:          strings.TrimSpace(os.Getenv("FRONTEND_URL")),
		APIBaseURL:           strings.TrimSpace(os.Getenv("API_BASE_URL")),
		allowedOriginLookup:  make(map[string]struct{}),
		allowedRedirectHosts: make(map[string]struct{}),
	}

	rawOrigins := strings.TrimSpace(os.Getenv("CORS_ALLOWED_ORIGINS"))
	if rawOrigins == "" {
		if alias := strings.TrimSpace(os.Getenv("ALLOWED_ORIGINS")); alias != "" {
			rawOrigins = alias
			log.Warn().Msg("ALLOWED_ORIGINS is deprecated; prefer CORS_ALLOWED_ORIGINS or FRONTEND_URL")
		}
	}

	if rawOrigins != "" {
		for _, rawOrigin := range strings.Split(rawOrigins, ",") {
			cfg.addAllowedOrigin(rawOrigin)
		}
	}

	// FRONTEND_URL is the canonical browser origin and should always be allowed
	// when present, even if explicit CORS origins are configured.
	cfg.addAllowedOrigin(cfg.FrontendURL)

	if host, ok := canonicalHost(cfg.FrontendURL); ok {
		addRedirectHostWithLoopback(cfg.allowedRedirectHosts, host)
	}

	if host, ok := canonicalHost(os.Getenv("APP_HOST")); ok {
		addRedirectHostWithLoopback(cfg.allowedRedirectHosts, host)
	}

	for _, rawHost := range strings.Split(os.Getenv("GOOGLE_ALLOWED_REDIRECT_HOSTS"), ",") {
		if host, ok := canonicalHost(rawHost); ok {
			addRedirectHostWithLoopback(cfg.allowedRedirectHosts, host)
		}
	}

	if len(cfg.allowedOrigins) == 0 {
		log.Warn().Msg("no allowed origins configured; all browser CORS/CSRF requests will be rejected - set FRONTEND_URL or CORS_ALLOWED_ORIGINS")
	}

	return cfg
}

func (c *Config) addAllowedOrigin(raw string) {
	origin, ok := CanonicalOrigin(raw)
	if !ok {
		if trimmed := strings.TrimSpace(raw); trimmed != "" {
			log.Warn().Str("origin", trimmed).Msg("ignoring malformed allowed origin")
		}
		return
	}

	c.insertOrigin(origin)

	if alt := loopbackEquivalent(origin); alt != "" {
		c.insertOrigin(alt)
	}
}

func (c *Config) insertOrigin(origin string) {
	if _, exists := c.allowedOriginLookup[origin]; exists {
		return
	}
	c.allowedOriginLookup[origin] = struct{}{}
	c.allowedOrigins = append(c.allowedOrigins, origin)
}

// AllowedOrigins returns the configured set of browser-allowed origins.
func (c Config) AllowedOrigins() []string {
	out := make([]string, len(c.allowedOrigins))
	copy(out, c.allowedOrigins)
	return out
}

// AllowedOrigins loads config from environment and returns the allowed origins.
func AllowedOrigins() []string {
	return LoadConfigFromEnv().AllowedOrigins()
}

// IsAllowedOrigin returns true when the supplied origin matches the canonical config.
func (c Config) IsAllowedOrigin(raw string) bool {
	origin, ok := CanonicalOrigin(raw)
	if !ok {
		return false
	}

	_, exists := c.allowedOriginLookup[origin]
	return exists
}

// IsAllowedOrigin loads config from environment and checks the origin.
func IsAllowedOrigin(raw string) bool {
	return LoadConfigFromEnv().IsAllowedOrigin(raw)
}

// IsAllowedRedirectURI validates an OAuth redirect URI against the canonical
// frontend/browser origins and any explicitly configured redirect hosts.
func (c Config) IsAllowedRedirectURI(raw string) bool {
	u, err := url.Parse(strings.TrimSpace(raw))
	if err != nil || u.Host == "" {
		return false
	}

	if c.IsAllowedOrigin(u.Scheme + "://" + u.Host) {
		return true
	}

	host := strings.ToLower(strings.TrimSpace(u.Hostname()))
	if host == "" {
		return false
	}

	_, exists := c.allowedRedirectHosts[host]
	return exists
}

// IsAllowedRedirectURI loads config from environment and validates the redirect URI.
func IsAllowedRedirectURI(raw string) bool {
	return LoadConfigFromEnv().IsAllowedRedirectURI(raw)
}

// loopbackEquivalent returns the localhost<->127.0.0.1 counterpart of an
// origin so that both spellings are accepted transparently.
func loopbackEquivalent(origin string) string {
	u, err := url.Parse(origin)
	if err != nil || u.Host == "" {
		return ""
	}
	host := u.Hostname()
	port := u.Port()
	var altHost string
	if host == "localhost" {
		altHost = "127.0.0.1"
	} else if host == "127.0.0.1" {
		altHost = "localhost"
	} else {
		return ""
	}
	if port != "" {
		return u.Scheme + "://" + altHost + ":" + port
	}
	return u.Scheme + "://" + altHost
}

func addRedirectHostWithLoopback(hosts map[string]struct{}, host string) {
	hosts[host] = struct{}{}
	if host == "localhost" {
		hosts["127.0.0.1"] = struct{}{}
	} else if host == "127.0.0.1" {
		hosts["localhost"] = struct{}{}
	}
}

func canonicalHost(raw string) (string, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return "", false
	}

	if strings.Contains(raw, "://") {
		u, err := url.Parse(raw)
		if err != nil || u.Host == "" {
			return "", false
		}
		return strings.ToLower(strings.TrimSpace(u.Hostname())), true
	}

	return strings.ToLower(raw), true
}
