package hosted

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/mail/credentials"
	"github.com/ultisuite/ulti-backend/internal/mail/stalwart"
)

var (
	ErrAddressTaken     = errors.New("mail address already taken")
	ErrInvalidLocalPart = errors.New("invalid local part")
	ErrDomainNotActive  = errors.New("mail domain not active")
)

type Service struct {
	db      *pgxpool.Pool
	stlw    *stalwart.Client
	creds   *credentials.Manager
	imapHost string
	imapPort int
	imapTLS  bool
	smtpHost string
	smtpPort int
	smtpTLS  bool
}

func NewService(db *pgxpool.Pool, stlw *stalwart.Client, creds *credentials.Manager) *Service {
	s := &Service{db: db, stlw: stlw, creds: creds}
	if stlw != nil {
		s.imapHost, s.imapPort, s.imapTLS = stlw.IMAPEndpoint()
		s.smtpHost, s.smtpPort, s.smtpTLS = stlw.SMTPEndpoint()
	}
	return s
}

type DomainRow struct {
	ID                 string  `json:"id"`
	Name               string  `json:"name"`
	Status             string  `json:"status"`
	VerificationToken  string  `json:"verification_token,omitempty"`
	DKIMSelector       string  `json:"dkim_selector,omitempty"`
	DKIMPublicKey      string  `json:"dkim_public_key,omitempty"`
	StalwartDomainID   string  `json:"stalwart_domain_id,omitempty"`
	IsPlatformDomain   bool    `json:"is_platform_domain"`
	MXVerifiedAt       *string `json:"mx_verified_at,omitempty"`
	TXTVerifiedAt      *string `json:"txt_verified_at,omitempty"`
	CreatedAt          string  `json:"created_at"`
}

type MailboxRow struct {
	ID               string `json:"id"`
	DomainID         string `json:"domain_id"`
	LocalPart        string `json:"local_part"`
	Email            string `json:"email"`
	UserID           string `json:"user_id,omitempty"`
	MailAccountID    string `json:"mail_account_id,omitempty"`
	StalwartAccountID string `json:"stalwart_account_id,omitempty"`
	QuotaBytes       int64  `json:"quota_bytes"`
	Status           string `json:"status"`
}

func normalizeLocalPart(v string) (string, error) {
	v = strings.ToLower(strings.TrimSpace(v))
	if v == "" || len(v) > 64 {
		return "", ErrInvalidLocalPart
	}
	for _, ch := range v {
		if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == '_' || ch == '+' {
			continue
		}
		return "", ErrInvalidLocalPart
	}
	return v, nil
}

type EndpointInfo struct {
	IMAPHost string `json:"imap_host"`
	IMAPPort int    `json:"imap_port"`
	IMAPTLS  bool   `json:"imap_tls"`
	SMTPHost string `json:"smtp_host"`
	SMTPPort int    `json:"smtp_port"`
	SMTPTLS  bool   `json:"smtp_tls"`
}

func (s *Service) Available() bool {
	return s != nil && s.stlw != nil && s.stlw.Enabled()
}

func (s *Service) Endpoints() EndpointInfo {
	return EndpointInfo{
		IMAPHost: s.imapHost,
		IMAPPort: s.imapPort,
		IMAPTLS:  s.imapTLS,
		SMTPHost: s.smtpHost,
		SMTPPort: s.smtpPort,
		SMTPTLS:  s.smtpTLS,
	}
}

func (s *Service) GetPlatformDomain(ctx context.Context) (DomainRow, error) {
	var row DomainRow
	err := s.db.QueryRow(ctx, `
		SELECT id::text, name, status, verification_token, dkim_selector, dkim_public_key,
		       stalwart_domain_id, is_platform_domain,
		       mx_verified_at::text, txt_verified_at::text, created_at::text
		FROM mail_domains
		WHERE is_platform_domain = true
		ORDER BY created_at ASC
		LIMIT 1
	`).Scan(
		&row.ID, &row.Name, &row.Status, &row.VerificationToken, &row.DKIMSelector, &row.DKIMPublicKey,
		&row.StalwartDomainID, &row.IsPlatformDomain, &row.MXVerifiedAt, &row.TXTVerifiedAt, &row.CreatedAt,
	)
	if err != nil {
		return DomainRow{}, err
	}
	return row, nil
}

func (s *Service) GetUserMailbox(ctx context.Context, userID string) (MailboxRow, error) {
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return MailboxRow{}, fmt.Errorf("user id required")
	}
	var row MailboxRow
	err := s.db.QueryRow(ctx, `
		SELECT mb.id::text, mb.domain_id::text, mb.local_part,
		       lower(mb.local_part || '@' || md.name),
		       COALESCE(mb.user_id::text, ''),
		       COALESCE(mb.mail_account_id::text, ''),
		       mb.stalwart_account_id, mb.quota_bytes, mb.status
		FROM mailboxes mb
		JOIN mail_domains md ON md.id = mb.domain_id
		WHERE mb.user_id = $1::uuid
		ORDER BY mb.created_at ASC
		LIMIT 1
	`, userID).Scan(
		&row.ID, &row.DomainID, &row.LocalPart, &row.Email,
		&row.UserID, &row.MailAccountID, &row.StalwartAccountID, &row.QuotaBytes, &row.Status,
	)
	return row, err
}

func (s *Service) IsAddressAvailable(ctx context.Context, domainName, localPart string) (bool, error) {
	localPart, err := normalizeLocalPart(localPart)
	if err != nil {
		return false, err
	}
	domainName = strings.ToLower(strings.TrimSpace(domainName))
	var exists bool
	err = s.db.QueryRow(ctx, `
		SELECT EXISTS(
			SELECT 1 FROM mailboxes m
			JOIN mail_domains d ON d.id = m.domain_id
			WHERE d.name = $1 AND m.local_part = $2
		)
	`, domainName, localPart).Scan(&exists)
	return !exists, err
}

func (s *Service) EnsurePlatformDomain(ctx context.Context, name string) (DomainRow, error) {
	name = strings.ToLower(strings.TrimSpace(name))
	if name == "" {
		return DomainRow{}, fmt.Errorf("platform domain name required")
	}
	var row DomainRow
	err := s.db.QueryRow(ctx, `
		SELECT id::text, name, status, verification_token, dkim_selector, dkim_public_key,
		       stalwart_domain_id, is_platform_domain,
		       mx_verified_at::text, txt_verified_at::text, created_at::text
		FROM mail_domains WHERE name = $1
	`, name).Scan(
		&row.ID, &row.Name, &row.Status, &row.VerificationToken, &row.DKIMSelector, &row.DKIMPublicKey,
		&row.StalwartDomainID, &row.IsPlatformDomain, &row.MXVerifiedAt, &row.TXTVerifiedAt, &row.CreatedAt,
	)
	if err == nil {
		return row, nil
	}
	if !errors.Is(err, pgx.ErrNoRows) {
		return DomainRow{}, err
	}
	token, err := randomToken(16)
	if err != nil {
		return DomainRow{}, err
	}
	stlwID := ""
	if s.stlw != nil && s.stlw.Enabled() {
		d, err := s.stlw.CreateDomain(ctx, name)
		if err != nil {
			return DomainRow{}, fmt.Errorf("stalwart create domain: %w", err)
		}
		stlwID = d.ID
	}
	status := "active"
	if !strings.HasSuffix(name, ".local") {
		status = "pending_verification"
	}
	err = s.db.QueryRow(ctx, `
		INSERT INTO mail_domains (name, status, verification_token, stalwart_domain_id, is_platform_domain, txt_verified_at)
		VALUES ($1, $2, $3, $4, true, CASE WHEN $2 = 'active' THEN NOW() ELSE NULL END)
		RETURNING id::text, name, status, verification_token, dkim_selector, dkim_public_key,
		          stalwart_domain_id, is_platform_domain,
		          mx_verified_at::text, txt_verified_at::text, created_at::text
	`, name, status, token, stlwID).Scan(
		&row.ID, &row.Name, &row.Status, &row.VerificationToken, &row.DKIMSelector, &row.DKIMPublicKey,
		&row.StalwartDomainID, &row.IsPlatformDomain, &row.MXVerifiedAt, &row.TXTVerifiedAt, &row.CreatedAt,
	)
	return row, err
}

func (s *Service) CreateDomain(ctx context.Context, name string, platform bool) (DomainRow, error) {
	name = strings.ToLower(strings.TrimSpace(name))
	if name == "" {
		return DomainRow{}, fmt.Errorf("domain name required")
	}
	token, err := randomToken(16)
	if err != nil {
		return DomainRow{}, err
	}
	stlwID := ""
	if s.stlw != nil && s.stlw.Enabled() {
		d, err := s.stlw.CreateDomain(ctx, name)
		if err != nil {
			return DomainRow{}, fmt.Errorf("stalwart create domain: %w", err)
		}
		stlwID = d.ID
	}
	var row DomainRow
	err = s.db.QueryRow(ctx, `
		INSERT INTO mail_domains (name, status, verification_token, stalwart_domain_id, is_platform_domain)
		VALUES ($1, 'pending_verification', $2, $3, $4)
		RETURNING id::text, name, status, verification_token, dkim_selector, dkim_public_key,
		          stalwart_domain_id, is_platform_domain,
		          mx_verified_at::text, txt_verified_at::text, created_at::text
	`, name, token, stlwID, platform).Scan(
		&row.ID, &row.Name, &row.Status, &row.VerificationToken, &row.DKIMSelector, &row.DKIMPublicKey,
		&row.StalwartDomainID, &row.IsPlatformDomain, &row.MXVerifiedAt, &row.TXTVerifiedAt, &row.CreatedAt,
	)
	return row, err
}

func (s *Service) ListDomains(ctx context.Context) ([]DomainRow, error) {
	rows, err := s.db.Query(ctx, `
		SELECT id::text, name, status, verification_token, dkim_selector, dkim_public_key,
		       stalwart_domain_id, is_platform_domain,
		       mx_verified_at::text, txt_verified_at::text, created_at::text
		FROM mail_domains ORDER BY is_platform_domain DESC, name ASC
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []DomainRow
	for rows.Next() {
		var row DomainRow
		if err := rows.Scan(
			&row.ID, &row.Name, &row.Status, &row.VerificationToken, &row.DKIMSelector, &row.DKIMPublicKey,
			&row.StalwartDomainID, &row.IsPlatformDomain, &row.MXVerifiedAt, &row.TXTVerifiedAt, &row.CreatedAt,
		); err != nil {
			return nil, err
		}
		out = append(out, row)
	}
	return out, rows.Err()
}

func (s *Service) GetDomain(ctx context.Context, domainID string) (DomainRow, error) {
	var row DomainRow
	err := s.db.QueryRow(ctx, `
		SELECT id::text, name, status, verification_token, dkim_selector, dkim_public_key,
		       stalwart_domain_id, is_platform_domain,
		       mx_verified_at::text, txt_verified_at::text, created_at::text
		FROM mail_domains WHERE id = $1
	`, domainID).Scan(
		&row.ID, &row.Name, &row.Status, &row.VerificationToken, &row.DKIMSelector, &row.DKIMPublicKey,
		&row.StalwartDomainID, &row.IsPlatformDomain, &row.MXVerifiedAt, &row.TXTVerifiedAt, &row.CreatedAt,
	)
	return row, err
}

func (s *Service) MarkDomainVerified(ctx context.Context, domainID string) (DomainRow, error) {
	var row DomainRow
	err := s.db.QueryRow(ctx, `
		UPDATE mail_domains
		SET status = 'active', txt_verified_at = COALESCE(txt_verified_at, NOW()), updated_at = NOW()
		WHERE id = $1
		RETURNING id::text, name, status, verification_token, dkim_selector, dkim_public_key,
		          stalwart_domain_id, is_platform_domain,
		          mx_verified_at::text, txt_verified_at::text, created_at::text
	`, domainID).Scan(
		&row.ID, &row.Name, &row.Status, &row.VerificationToken, &row.DKIMSelector, &row.DKIMPublicKey,
		&row.StalwartDomainID, &row.IsPlatformDomain, &row.MXVerifiedAt, &row.TXTVerifiedAt, &row.CreatedAt,
	)
	return row, err
}

func (s *Service) MarkDomainMXVerified(ctx context.Context, domainID string) (DomainRow, error) {
	var row DomainRow
	err := s.db.QueryRow(ctx, `
		UPDATE mail_domains
		SET mx_verified_at = NOW(), status = 'active', updated_at = NOW()
		WHERE id = $1
		RETURNING id::text, name, status, verification_token, dkim_selector, dkim_public_key,
		          stalwart_domain_id, is_platform_domain,
		          mx_verified_at::text, txt_verified_at::text, created_at::text
	`, domainID).Scan(
		&row.ID, &row.Name, &row.Status, &row.VerificationToken, &row.DKIMSelector, &row.DKIMPublicKey,
		&row.StalwartDomainID, &row.IsPlatformDomain, &row.MXVerifiedAt, &row.TXTVerifiedAt, &row.CreatedAt,
	)
	return row, err
}

type ProvisionMailboxInput struct {
	UserID      string
	Email       string
	DisplayName string
	Password    string
	QuotaBytes  int64
	DomainID    string
}

type ProvisionMailboxResult struct {
	Mailbox       MailboxRow
	MailAccountID string
}

// EnsureMailboxProvisioned creates a mailbox or links an existing one to the requested user.
func (s *Service) EnsureMailboxProvisioned(ctx context.Context, in ProvisionMailboxInput) (ProvisionMailboxResult, error) {
	email := strings.ToLower(strings.TrimSpace(in.Email))
	existing, err := s.lookupMailboxByEmail(ctx, email)
	if errors.Is(err, pgx.ErrNoRows) {
		return s.ProvisionMailbox(ctx, in)
	}
	if err != nil {
		return ProvisionMailboxResult{}, err
	}
	return s.reconcileExistingMailbox(ctx, existing, in)
}

func (s *Service) lookupMailboxByEmail(ctx context.Context, email string) (MailboxRow, error) {
	email = strings.ToLower(strings.TrimSpace(email))
	at := strings.LastIndex(email, "@")
	if at <= 0 {
		return MailboxRow{}, fmt.Errorf("invalid email")
	}
	localPart := email[:at]
	domainName := email[at+1:]
	localPart, err := normalizeLocalPart(localPart)
	if err != nil {
		return MailboxRow{}, err
	}
	var row MailboxRow
	err = s.db.QueryRow(ctx, `
		SELECT mb.id::text, mb.domain_id::text, mb.local_part,
		       lower(mb.local_part || '@' || md.name),
		       COALESCE(mb.user_id::text, ''),
		       COALESCE(mb.mail_account_id::text, ''),
		       mb.stalwart_account_id, mb.quota_bytes, mb.status
		FROM mailboxes mb
		JOIN mail_domains md ON md.id = mb.domain_id
		WHERE md.name = $1 AND mb.local_part = $2
	`, domainName, localPart).Scan(
		&row.ID, &row.DomainID, &row.LocalPart, &row.Email,
		&row.UserID, &row.MailAccountID, &row.StalwartAccountID, &row.QuotaBytes, &row.Status,
	)
	return row, err
}

func (s *Service) reconcileExistingMailbox(ctx context.Context, existing MailboxRow, in ProvisionMailboxInput) (ProvisionMailboxResult, error) {
	userID := strings.TrimSpace(in.UserID)
	if userID != "" && existing.UserID != "" && existing.UserID != userID {
		return ProvisionMailboxResult{}, ErrAddressTaken
	}
	if userID == "" {
		userID = existing.UserID
	}
	mailAccountID := existing.MailAccountID

	if userID != "" && existing.UserID != userID {
		if err := s.LinkMailboxToUser(ctx, existing.ID, userID); err != nil {
			return ProvisionMailboxResult{}, err
		}
		existing.UserID = userID
	}

	if userID != "" && mailAccountID == "" && strings.TrimSpace(in.Password) != "" {
		email := strings.ToLower(strings.TrimSpace(in.Email))
		err := s.db.QueryRow(ctx, `
			SELECT id::text FROM mail_accounts
			WHERE user_id = $1::uuid AND lower(email) = $2
			LIMIT 1
		`, userID, email).Scan(&mailAccountID)
		if err != nil && !errors.Is(err, pgx.ErrNoRows) {
			return ProvisionMailboxResult{}, err
		}
		if errors.Is(err, pgx.ErrNoRows) {
			enc, err := s.encryptHostedCredential(email, in.Password)
			if err != nil {
				return ProvisionMailboxResult{}, err
			}
			displayName := strings.TrimSpace(in.DisplayName)
			if displayName == "" {
				displayName = email
			}
			err = s.db.QueryRow(ctx, `
				INSERT INTO mail_accounts (
					user_id, name, email, provider,
					imap_host, imap_port, imap_tls,
					smtp_host, smtp_port, smtp_tls,
					credentials, is_active
				)
				VALUES ($1, $2, $3, 'hosted', $4, $5, $6, $7, $8, $9, $10, true)
				RETURNING id::text
			`, userID, displayName, email,
				s.imapHost, s.imapPort, s.imapTLS,
				s.smtpHost, s.smtpPort, s.smtpTLS,
				enc,
			).Scan(&mailAccountID)
			if err != nil {
				return ProvisionMailboxResult{}, err
			}
		}
		_, err = s.db.Exec(ctx, `
			UPDATE mailboxes SET mail_account_id = $1::uuid, updated_at = NOW()
			WHERE id = $2::uuid AND (mail_account_id IS NULL OR mail_account_id = $1::uuid)
		`, mailAccountID, existing.ID)
		if err != nil {
			return ProvisionMailboxResult{}, err
		}
		existing.MailAccountID = mailAccountID
	}

	return ProvisionMailboxResult{
		Mailbox:       existing,
		MailAccountID: mailAccountID,
	}, nil
}

func (s *Service) ProvisionMailbox(ctx context.Context, in ProvisionMailboxInput) (ProvisionMailboxResult, error) {
	email := strings.ToLower(strings.TrimSpace(in.Email))
	at := strings.LastIndex(email, "@")
	if at <= 0 {
		return ProvisionMailboxResult{}, fmt.Errorf("invalid email")
	}
	localPart := email[:at]
	domainName := email[at+1:]
	localPart, err := normalizeLocalPart(localPart)
	if err != nil {
		return ProvisionMailboxResult{}, err
	}

	var domain DomainRow
	if strings.TrimSpace(in.DomainID) != "" {
		domain, err = s.GetDomain(ctx, in.DomainID)
		if err != nil {
			return ProvisionMailboxResult{}, err
		}
		if !strings.EqualFold(domain.Name, domainName) {
			return ProvisionMailboxResult{}, fmt.Errorf("email domain %q does not match project domain %q", domainName, domain.Name)
		}
	} else {
		err = s.db.QueryRow(ctx, `
			SELECT id::text, name, status, verification_token, dkim_selector, dkim_public_key,
			       stalwart_domain_id, is_platform_domain,
			       mx_verified_at::text, txt_verified_at::text, created_at::text
			FROM mail_domains WHERE name = $1
		`, domainName).Scan(
			&domain.ID, &domain.Name, &domain.Status, &domain.VerificationToken, &domain.DKIMSelector, &domain.DKIMPublicKey,
			&domain.StalwartDomainID, &domain.IsPlatformDomain, &domain.MXVerifiedAt, &domain.TXTVerifiedAt, &domain.CreatedAt,
		)
		if err != nil {
			if errors.Is(err, pgx.ErrNoRows) {
				domain, err = s.EnsurePlatformDomain(ctx, domainName)
				if err != nil {
					return ProvisionMailboxResult{}, err
				}
			} else {
				return ProvisionMailboxResult{}, err
			}
		}
	}
	if domain.Status != "active" && !domain.IsPlatformDomain {
		return ProvisionMailboxResult{}, ErrDomainNotActive
	}

	available, err := s.IsAddressAvailable(ctx, domainName, localPart)
	if err != nil {
		return ProvisionMailboxResult{}, err
	}
	if !available {
		return ProvisionMailboxResult{}, ErrAddressTaken
	}

	quota := in.QuotaBytes
	if quota <= 0 {
		quota = 5 * 1024 * 1024 * 1024
	}

	stlwAccountID := ""
	if s.stlw != nil {
		acct, err := s.stlw.CreateAccount(ctx, domain.StalwartDomainID, localPart, in.Password, quota)
		if err != nil && !errors.Is(err, stalwart.ErrDisabled) {
			return ProvisionMailboxResult{}, fmt.Errorf("stalwart account: %w", err)
		}
		stlwAccountID = acct.ID
	}

	tx, err := s.db.Begin(ctx)
	if err != nil {
		return ProvisionMailboxResult{}, err
	}
	defer tx.Rollback(ctx)

	var mailboxID string
	err = tx.QueryRow(ctx, `
		INSERT INTO mailboxes (domain_id, local_part, user_id, stalwart_account_id, quota_bytes, status)
		VALUES ($1, $2, NULLIF($3, '')::uuid, $4, $5, 'active')
		RETURNING id::text
	`, domain.ID, localPart, in.UserID, stlwAccountID, quota).Scan(&mailboxID)
	if err != nil {
		return ProvisionMailboxResult{}, err
	}

	var mailAccountID string
	if in.UserID != "" {
		enc, err := s.encryptHostedCredential(email, in.Password)
		if err != nil {
			return ProvisionMailboxResult{}, err
		}
		err = tx.QueryRow(ctx, `
			INSERT INTO mail_accounts (
				user_id, name, email, provider,
				imap_host, imap_port, imap_tls,
				smtp_host, smtp_port, smtp_tls,
				credentials, is_active
			)
			VALUES ($1, $2, $3, 'hosted', $4, $5, $6, $7, $8, $9, $10, true)
			RETURNING id::text
		`, in.UserID, in.DisplayName, email,
			s.imapHost, s.imapPort, s.imapTLS,
			s.smtpHost, s.smtpPort, s.smtpTLS,
			enc,
		).Scan(&mailAccountID)
		if err != nil {
			return ProvisionMailboxResult{}, err
		}
		_, err = tx.Exec(ctx, `
			UPDATE mailboxes SET user_id = $1::uuid, mail_account_id = $2::uuid, updated_at = NOW()
			WHERE id = $3::uuid
		`, in.UserID, mailAccountID, mailboxID)
		if err != nil {
			return ProvisionMailboxResult{}, err
		}
	}

	if err := tx.Commit(ctx); err != nil {
		return ProvisionMailboxResult{}, err
	}

	return ProvisionMailboxResult{
		Mailbox: MailboxRow{
			ID:                mailboxID,
			DomainID:          domain.ID,
			LocalPart:         localPart,
			Email:             email,
			UserID:            in.UserID,
			MailAccountID:     mailAccountID,
			StalwartAccountID: stlwAccountID,
			QuotaBytes:        quota,
			Status:            "active",
		},
		MailAccountID: mailAccountID,
	}, nil
}

func (s *Service) encryptHostedCredential(email, password string) ([]byte, error) {
	if s.creds == nil {
		return nil, fmt.Errorf("credential manager not configured")
	}
	return s.creds.EncryptCredential(credentials.Credential{
		AuthType: credentials.AuthPassword,
		Username: email,
		Password: password,
	})
}

func (s *Service) LinkMailboxToUser(ctx context.Context, mailboxID, userID string) error {
	_, err := s.db.Exec(ctx, `
		UPDATE mailboxes SET user_id = $1::uuid, updated_at = NOW()
		WHERE id = $2::uuid AND (user_id IS NULL OR user_id = $1::uuid)
	`, userID, mailboxID)
	return err
}

func randomToken(n int) (string, error) {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

func NewInviteToken() (string, error) {
	return uuid.NewString(), nil
}
