package handlers

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

// Enricher looks up supplementary fields (tenant name, owner email) that
// are not carried in cross-service event payloads. Provisioning day-2
// events (provision.app_ready / app_removed / app_failed) only carry
// tenant_id + app_slug; the email template needs the human-readable org
// name and the recipient address.
//
// Implementation calls the tenant and auth services over the internal
// cluster network using a short-lived superadmin JWT minted with the
// shared JWT_SECRET. Only services inside the sme namespace can hit
// these endpoints; the gateway does not expose /tenant/admin or
// /auth/admin.
type Enricher struct {
	TenantURL string // e.g. http://tenant.sme.svc.cluster.local:8083
	AuthURL   string // e.g. http://auth.sme.svc.cluster.local:8081
	// ParentZone is the Sovereign's sme-pool parent domain (e.g.
	// "omani.homes" / "talents.scope") used to render WorkspaceURL.
	// TBD-A67 issue #1990 removed the hardcoded `.openova.io` suffix —
	// the parent zone is per-Sovereign and the notification service
	// reads it from the TENANT_PARENT_DOMAIN env (same env name the
	// provisioning service uses via Handler.TenantParentDomain). Empty
	// disables the WorkspaceURL field so the email template can fall
	// back to a bare-org-name body rather than emit a wrong URL.
	ParentZone string
	JWTSecret  []byte
	HTTP       *http.Client
}

// NewEnricher constructs an Enricher. Leave URLs empty to disable — in
// that case Lookup returns zero values without error and the caller
// will skip the email. parentZone is the Sovereign's sme-pool parent
// domain used to build per-tenant WorkspaceURL strings; empty parent
// zone results in an empty WorkspaceURL (template-side guard).
// TBD-A67 issue #1990: NEVER hardcode `.openova.io` here — every
// Sovereign owns its own parent zone.
func NewEnricher(tenantURL, authURL, parentZone string, jwtSecret []byte) *Enricher {
	return &Enricher{
		TenantURL:  tenantURL,
		AuthURL:    authURL,
		ParentZone: parentZone,
		JWTSecret:  jwtSecret,
		HTTP:       &http.Client{Timeout: 5 * time.Second},
	}
}

// TenantInfo is the subset of tenant+owner fields needed for email bodies.
type TenantInfo struct {
	TenantID     string
	OrgName      string
	Subdomain    string
	WorkspaceURL string
	OwnerEmail   string
	OwnerName    string
}

// Lookup resolves a tenant_id into TenantInfo. Returns (nil, nil) if
// enrichment is disabled (no URLs configured) so callers can fall back
// to a skip-the-email path without treating missing config as an error.
func (e *Enricher) Lookup(ctx context.Context, tenantID string) (*TenantInfo, error) {
	if e == nil || e.TenantURL == "" || e.AuthURL == "" {
		return nil, nil
	}
	if tenantID == "" {
		return nil, fmt.Errorf("enrich: empty tenant_id")
	}

	token, err := e.serviceToken()
	if err != nil {
		return nil, fmt.Errorf("enrich: mint token: %w", err)
	}

	tenant, err := e.getTenant(ctx, tenantID, token)
	if err != nil {
		return nil, err
	}
	owner, err := e.getUser(ctx, tenant.OwnerID, token)
	if err != nil {
		return nil, err
	}
	return &TenantInfo{
		TenantID:     tenant.ID,
		OrgName:      tenant.Name,
		Subdomain:    tenant.Subdomain,
		WorkspaceURL: workspaceURL(tenant.Subdomain, e.ParentZone),
		OwnerEmail:   owner.Email,
		OwnerName:    owner.Name,
	}, nil
}

// workspaceURL renders the canonical per-tenant console URL using the
// Sovereign's parent zone — `https://console.<subdomain>.<parentZone>`.
// Mirrors sme_tenant_gitops.go:536 (chart-side host derivation) and
// tenant_route.go:113 (controller-side HTTPRoute hostname) so the
// notification email's clickable link lands on the exact host the
// Cilium Gateway routes to the tenant's console Service.
//
// TBD-A67 issue #1990: empty parent zone returns "" (caller / template
// is expected to omit the URL line). NEVER falls back to `.openova.io`
// — that hostname is the platform marketing domain, not a tenant
// console host, and hardcoding it leaked into email bodies on every
// non-openova.io Sovereign (omani.homes, talents.scope, …) violating
// the "never touch openova.io" rule.
func workspaceURL(subdomain, parentZone string) string {
	subdomain = strings.TrimSpace(subdomain)
	parentZone = strings.TrimSpace(parentZone)
	if subdomain == "" || parentZone == "" {
		return ""
	}
	return "https://console." + subdomain + "." + parentZone
}

// serviceToken mints a short-lived superadmin JWT that the tenant and
// auth services trust via the shared JWT_SECRET. Lifetime is kept
// deliberately short (2 minutes) — long enough for an event-handler
// chain but too short to be useful if exfiltrated from logs.
func (e *Enricher) serviceToken() (string, error) {
	now := time.Now()
	claims := jwt.MapClaims{
		"sub":  "svc:notification",
		"role": "superadmin",
		"iat":  now.Unix(),
		"exp":  now.Add(2 * time.Minute).Unix(),
	}
	tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return tok.SignedString(e.JWTSecret)
}

// tenantRecord mirrors services/tenant/store.Tenant for the fields we read.
type tenantRecord struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	OwnerID   string `json:"owner_id"`
	Subdomain string `json:"subdomain"`
}

func (e *Enricher) getTenant(ctx context.Context, tenantID, token string) (*tenantRecord, error) {
	url := e.TenantURL + "/tenant/admin/tenants/" + tenantID
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("enrich: build tenant req: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+token)
	resp, err := e.HTTP.Do(req)
	if err != nil {
		return nil, fmt.Errorf("enrich: tenant req: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("enrich: tenant lookup %s returned %d", tenantID, resp.StatusCode)
	}
	var out tenantRecord
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, fmt.Errorf("enrich: decode tenant: %w", err)
	}
	return &out, nil
}

// userResponse mirrors the wrapper produced by auth's AdminGetUser.
type userResponse struct {
	User struct {
		ID    string `json:"id"`
		Email string `json:"email"`
		Name  string `json:"name"`
	} `json:"user"`
}

// userRecord is the unwrapped user we care about.
type userRecord struct {
	ID    string
	Email string
	Name  string
}

func (e *Enricher) getUser(ctx context.Context, userID, token string) (*userRecord, error) {
	url := e.AuthURL + "/auth/admin/users/" + userID
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("enrich: build user req: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+token)
	resp, err := e.HTTP.Do(req)
	if err != nil {
		return nil, fmt.Errorf("enrich: user req: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("enrich: user lookup %s returned %d", userID, resp.StatusCode)
	}
	var out userResponse
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, fmt.Errorf("enrich: decode user: %w", err)
	}
	return &userRecord{ID: out.User.ID, Email: out.User.Email, Name: out.User.Name}, nil
}
