package notify

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// Provisioner implements port.NotifyProvisioner using the notify admin API.
// Each project gets an isolated sending host (mail.{slug}.{baseDomain}),
// a Resend domain with DKIM/SPF DNS records, and a dedicated send key.
type Provisioner struct {
	client       notifyAdminAPI
	resend       resendAPI        // nil when ResendAPIKey not configured
	resendAPIKey string           // passed to createProvider; kept separate from resend for interface compatibility
	dns          port.DNSProvider // nil when Cloudflare not configured
	baseDomain   string           // e.g., "threesix.ai"
	logger       *slog.Logger
}

// Config holds configuration for the notify provisioner.
type Config struct {
	BaseURL      string // Required: notify service URL (e.g., "https://notify.orchard9.ai")
	AdminKey     string // Required: admin API key (notify_admin_...)
	ResendAPIKey string // Optional: Resend API key for per-project domain provisioning
	BaseDomain   string // Base domain for per-project hosts (default: "threesix.ai")
}

// NewProvisioner creates a new notify provisioner.
func NewProvisioner(cfg Config, dns port.DNSProvider, logger *slog.Logger) *Provisioner {
	baseDomain := cfg.BaseDomain
	if baseDomain == "" {
		baseDomain = "threesix.ai"
	}
	p := &Provisioner{
		client:     newAdminClient(cfg.BaseURL, cfg.AdminKey),
		dns:        dns,
		baseDomain: baseDomain,
		logger:     logger,
	}
	if cfg.ResendAPIKey != "" {
		p.resend = newResendClient(cfg.ResendAPIKey)
		p.resendAPIKey = cfg.ResendAPIKey
	}
	return p
}

// CreateProjectNotify provisions a per-project notify host, Resend domain, DNS records,
// and notify account with send key.
//
// Steps:
//  1. Create notify host mail.{slug}.{baseDomain}
//  2. Add Resend provider to the host (skipped if ResendAPIKey not configured)
//  3. Register from-address noreply@mail.{slug}.{baseDomain}
//  4. Create notify account "project-{projectID}"
//  5. Create send key for the account
//  6. Grant the account access to the host (non-fatal)
//  7. Create Resend domain (non-fatal — skipped if ResendAPIKey not configured)
//  8. Add DNS records via Cloudflare (non-fatal — skipped if DNS not configured)
//  9. Fire-and-forget async domain verification
func (p *Provisioner) CreateProjectNotify(ctx context.Context, projectID, slug string) (*domain.NotifyCredentials, error) {
	host := "mail." + slug + "." + p.baseDomain
	from := "noreply@" + host
	accountName := "project-" + projectID

	// 1. Create notify host
	if err := p.client.createHost(ctx, host, "failover"); err != nil {
		return nil, fmt.Errorf("notify: create host %s for project %s: %w", host, projectID, err)
	}

	// 2. Add Resend provider to the host (only when Resend is configured)
	if p.resend != nil {
		if err := p.client.createProvider(ctx, host, "resend", map[string]string{"api_key": p.resendAPIKey}, 1, 3, 1000); err != nil {
			p.bestEffortDeleteHost(ctx, host, projectID)
			return nil, fmt.Errorf("notify: create provider on host %s for project %s: %w", host, projectID, err)
		}
	}

	// 3. Register from-address
	if err := p.client.createFromAddress(ctx, host, from, slug); err != nil {
		p.bestEffortDeleteHost(ctx, host, projectID)
		return nil, fmt.Errorf("notify: create from-address %s for project %s: %w", from, projectID, err)
	}

	// 4. Create account
	acct, err := p.client.createAccount(ctx, accountName)
	if err != nil {
		p.bestEffortDeleteHost(ctx, host, projectID)
		return nil, fmt.Errorf("notify: create account for project %s: %w", projectID, err)
	}

	// 5. Create send key
	key, err := p.client.createSendKey(ctx, acct.ID, accountName+"-send")
	if err != nil {
		p.bestEffortDeleteAccount(ctx, acct.ID, projectID)
		p.bestEffortDeleteHost(ctx, host, projectID)
		return nil, fmt.Errorf("notify: create send key for project %s: %w", projectID, err)
	}

	// 6. Grant host access (non-fatal — log warn and continue)
	if err := p.client.grantHostAccess(ctx, host, acct.ID); err != nil {
		p.logger.Warn("failed to grant notify host access",
			"host", host,
			"account_id", acct.ID,
			"project_id", projectID,
			"error", err,
		)
	}

	// 7. Create Resend domain (non-fatal — project still usable, email won't send until fixed)
	var resendDomainID string
	var dnsRecords []resendDNSRecord
	if p.resend != nil {
		var resendErr error
		resendDomainID, dnsRecords, resendErr = p.resend.createDomain(ctx, host, "us-east-1")
		if resendErr != nil {
			p.logger.Warn("failed to create resend domain — email delivery will not work until resolved",
				"host", host,
				"project_id", projectID,
				"error", resendErr,
			)
		} else {
			p.logger.Info("resend domain created", "host", host, "domain_id", resendDomainID)
		}
	}

	// 8. Add DNS records for DKIM/SPF (non-fatal).
	// Resend returns record names relative to the registered domain; build FQDNs for Cloudflare.
	// Cloudflare's normalizeName handles FQDNs ending in the zone name correctly.
	if p.dns != nil && len(dnsRecords) > 0 {
		for _, rec := range dnsRecords {
			fqdn := rec.Name + "." + host
			dnsRec := domain.DNSRecord{
				Type:    rec.Record,
				Name:    fqdn,
				Content: rec.Value,
				TTL:     1,
			}
			if _, upsertErr := p.dns.UpsertRecord(ctx, dnsRec); upsertErr != nil {
				p.logger.Warn("failed to upsert notify DNS record",
					"name", fqdn,
					"record", rec.Record,
					"project_id", projectID,
					"error", upsertErr,
				)
			}
		}
	}

	// 9. Fire-and-forget async domain verification
	if p.resend != nil && resendDomainID != "" {
		go func() {
			verifyCtx := context.WithoutCancel(ctx)
			if err := p.resend.verifyDomain(verifyCtx, resendDomainID); err != nil {
				p.logger.Warn("async resend domain verification failed",
					"domain_id", resendDomainID,
					"host", host,
					"error", err,
				)
			}
		}()
	}

	p.logger.Info("notify provisioned",
		"project_id", projectID,
		"host", host,
		"resend_domain_id", resendDomainID,
	)

	return &domain.NotifyCredentials{
		ProjectID:      projectID,
		AccountID:      acct.ID,
		APIKey:         key.Key,
		Host:           host,
		From:           from,
		ResendDomainID: resendDomainID,
		CreatedAt:      time.Now(),
	}, nil
}

// DeleteProjectNotify removes all notify resources for a project.
// Failures are logged as warnings — cleanup continues regardless.
func (p *Provisioner) DeleteProjectNotify(ctx context.Context, projectID, slug, resendDomainID string) error {
	host := "mail." + slug + "." + p.baseDomain

	// 1. Delete notify account (cascades keys + host grants)
	acct, err := p.findAccountByProject(ctx, projectID)
	if err != nil {
		p.logger.Warn("failed to find notify account during deletion",
			"project_id", projectID,
			"error", err,
		)
	} else if acct != nil {
		if err := p.client.deleteAccount(ctx, acct.ID); err != nil {
			p.logger.Warn("failed to delete notify account",
				"account_id", acct.ID,
				"project_id", projectID,
				"error", err,
			)
		}
	}

	// 2. Delete notify host
	if err := p.client.deleteHost(ctx, host); err != nil {
		p.logger.Warn("failed to delete notify host",
			"host", host,
			"project_id", projectID,
			"error", err,
		)
	}

	// 3. Delete Resend domain
	if p.resend != nil && resendDomainID != "" {
		if err := p.resend.deleteDomain(ctx, resendDomainID); err != nil {
			p.logger.Warn("failed to delete resend domain",
				"domain_id", resendDomainID,
				"project_id", projectID,
				"error", err,
			)
		}
	}

	// 4. Delete Cloudflare DNS records for DKIM/SPF.
	// Names follow Resend's standard format:
	//   DKIM:    resend._domainkey.{host}
	//   SPF MX:  send.{host}
	//   SPF TXT: send.{host}
	// If Resend changes their record naming, manual cleanup may be needed.
	if p.dns != nil {
		dkimName := "resend._domainkey." + host
		if err := p.dns.DeleteRecordByName(ctx, "TXT", dkimName); err != nil {
			p.logger.Warn("failed to delete DKIM DNS record",
				"name", dkimName,
				"project_id", projectID,
				"error", err,
			)
		}
		spfSendName := "send." + host
		if err := p.dns.DeleteRecordByName(ctx, "MX", spfSendName); err != nil {
			p.logger.Warn("failed to delete SPF MX DNS record",
				"name", spfSendName,
				"project_id", projectID,
				"error", err,
			)
		}
		if err := p.dns.DeleteRecordByName(ctx, "TXT", spfSendName); err != nil {
			p.logger.Warn("failed to delete SPF TXT DNS record",
				"name", spfSendName,
				"project_id", projectID,
				"error", err,
			)
		}
	}

	p.logger.Info("notify resources deleted", "project_id", projectID, "host", host)
	return nil
}

// GetProjectNotify returns notify credentials for the project, or nil if not provisioned.
// Note: Only AccountID and CreatedAt are populated — APIKey, Host, and From are not
// recoverable after provisioning. Use this method solely to check whether provisioning
// has already occurred (non-nil return = already provisioned).
func (p *Provisioner) GetProjectNotify(ctx context.Context, projectID string) (*domain.NotifyCredentials, error) {
	acct, err := p.findAccountByProject(ctx, projectID)
	if err != nil {
		return nil, fmt.Errorf("notify: find account for project %s: %w", projectID, err)
	}
	if acct == nil {
		return nil, nil
	}

	return &domain.NotifyCredentials{
		ProjectID: projectID,
		AccountID: acct.ID,
		CreatedAt: acct.CreatedAt,
	}, nil
}

// TestConnection verifies the notify admin API is reachable.
func (p *Provisioner) TestConnection(ctx context.Context) error {
	_, err := p.client.listAccounts(ctx)
	if err != nil {
		return fmt.Errorf("notify admin API unreachable: %w", err)
	}
	return nil
}

// findAccountByProject looks up the account named "project-{projectID}".
func (p *Provisioner) findAccountByProject(ctx context.Context, projectID string) (*accountResponse, error) {
	accounts, err := p.client.listAccounts(ctx)
	if err != nil {
		return nil, err
	}

	targetName := "project-" + projectID
	for i := range accounts {
		if accounts[i].Name == targetName {
			return &accounts[i], nil
		}
	}
	return nil, nil
}

// bestEffortDeleteHost deletes the notify host, logging on failure.
func (p *Provisioner) bestEffortDeleteHost(ctx context.Context, host, projectID string) {
	if err := p.client.deleteHost(ctx, host); err != nil {
		p.logger.Warn("failed to clean up notify host after provisioning failure",
			"host", host,
			"project_id", projectID,
			"error", err,
		)
	}
}

// bestEffortDeleteAccount deletes the notify account, logging on failure.
func (p *Provisioner) bestEffortDeleteAccount(ctx context.Context, accountID, projectID string) {
	if err := p.client.deleteAccount(ctx, accountID); err != nil {
		p.logger.Warn("failed to clean up notify account after provisioning failure",
			"account_id", accountID,
			"project_id", projectID,
			"error", err,
		)
	}
}
