// Package tenantstore manages multi-tenancy data: tenants, their domains, and
// per-tenant LDAP configuration. Phase 1 implements the core data layer;
// tenant isolation of mail data is handled in later phases.
package tenantstore

import (
	"context"
	"fmt"
	"time"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

// Tenant represents an organisational unit (company / department) in the system.
type Tenant struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	Slug      string    `json:"slug"`
	Active    bool      `json:"active"`
	CreatedAt time.Time `json:"created_at"`
	// Computed fields populated by List.
	DomainCount   int    `json:"domain_count,omitempty"`
	UserCount     int    `json:"user_count,omitempty"`
	LDAPEnabled   *bool   `json:"ldap_enabled,omitempty"`
	LDAPURL       *string `json:"ldap_url,omitempty"`
	HasLogo       bool   `json:"has_logo,omitempty"`
	RetentionDays int    `json:"retention_days"` // 0 = use global config
	// Quota fields (PROJ-29) — nil = unlimited
	MaxStorageBytes *int64 `json:"max_storage_bytes,omitempty"`
	MaxUsers        *int   `json:"max_users,omitempty"`
	MaxEmails       *int64 `json:"max_emails,omitempty"`
}

// TenantDomain is an e-mail domain assigned to a tenant.
type TenantDomain struct {
	ID        int64     `json:"id"`
	TenantID  int64     `json:"tenant_id"`
	Domain    string    `json:"domain"`
	CreatedAt time.Time `json:"created_at"`
}

// Store manages tenant data in PostgreSQL.
type Store struct {
	pool *pgxpool.Pool
}

const schemaSQL = `
CREATE TABLE IF NOT EXISTS tenants (
    id         BIGSERIAL PRIMARY KEY,
    name       VARCHAR(255) UNIQUE NOT NULL,
    slug       VARCHAR(100) UNIQUE NOT NULL,
    active     BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS tenant_domains (
    id         BIGSERIAL PRIMARY KEY,
    tenant_id  BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    domain     VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS tenant_ldap (
    tenant_id       BIGINT PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
    enabled         BOOLEAN NOT NULL DEFAULT false,
    url             TEXT NOT NULL DEFAULT '',
    bind_dn         TEXT NOT NULL DEFAULT '',
    bind_password   BYTEA,
    base_dn         TEXT NOT NULL DEFAULT '',
    user_filter     TEXT NOT NULL DEFAULT '(sAMAccountName=%s)',
    tls             BOOLEAN NOT NULL DEFAULT false,
    tls_skip_verify BOOLEAN NOT NULL DEFAULT false,
    default_role    VARCHAR(20) NOT NULL DEFAULT 'user',
    group_mappings  JSONB NOT NULL DEFAULT '[]'
);

ALTER TABLE users ADD COLUMN IF NOT EXISTS tenant_id BIGINT REFERENCES tenants(id);
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS tenant_id BIGINT REFERENCES tenants(id);
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS logo_data BYTEA;
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS logo_content_type VARCHAR(100) NOT NULL DEFAULT '';
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS imap_mode TEXT NOT NULL DEFAULT 'personal';
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS retention_days INT NOT NULL DEFAULT 0;
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS max_storage_bytes BIGINT;
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS max_users INT;
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS max_emails BIGINT;
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS ocr_enabled BOOLEAN NOT NULL DEFAULT TRUE;
`

// New connects to PostgreSQL and initialises the tenant schema.
func New(dsn string) (*Store, error) {
	ctx := context.Background()
	pool, err := pgxpool.New(ctx, dsn)
	if err != nil {
		return nil, fmt.Errorf("tenantstore: connect: %w", err)
	}

	s := &Store{pool: pool}
	if err := s.initSchema(ctx); err != nil {
		pool.Close()
		return nil, fmt.Errorf("tenantstore: init schema: %w", err)
	}

	return s, nil
}

// Close releases the connection pool.
func (s *Store) Close() {
	s.pool.Close()
}

func (s *Store) initSchema(ctx context.Context) error {
	_, err := s.pool.Exec(ctx, schemaSQL)
	return err
}

// Create inserts a new tenant. name and slug must be unique.
func (s *Store) Create(ctx context.Context, name, slug string) (*Tenant, error) {
	var id int64
	err := s.pool.QueryRow(ctx,
		`INSERT INTO tenants (name, slug) VALUES ($1, $2) RETURNING id`,
		name, slug,
	).Scan(&id)
	if err != nil {
		return nil, fmt.Errorf("tenantstore: create: %w", err)
	}
	return s.Get(ctx, id)
}

// List returns all tenants with computed domain_count, user_count, and LDAP status.
func (s *Store) List(ctx context.Context) ([]Tenant, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT t.id, t.name, t.slug, t.active, t.created_at,
		       COUNT(DISTINCT td.id)         AS domain_count,
		       COUNT(DISTINCT u.id)          AS user_count,
		       tl.enabled                    AS ldap_enabled,
		       tl.url                        AS ldap_url,
		       (t.logo_data IS NOT NULL)     AS has_logo,
		       t.retention_days,
		       t.max_storage_bytes,
		       t.max_users,
		       t.max_emails
		FROM tenants t
		LEFT JOIN tenant_domains td ON td.tenant_id = t.id
		LEFT JOIN users u           ON u.tenant_id  = t.id
		LEFT JOIN tenant_ldap tl    ON tl.tenant_id = t.id
		GROUP BY t.id, tl.enabled, tl.url
		ORDER BY t.id
	`)
	if err != nil {
		return nil, fmt.Errorf("tenantstore: list: %w", err)
	}
	defer rows.Close()

	var tenants []Tenant
	for rows.Next() {
		var t Tenant
		if err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Active, &t.CreatedAt, &t.DomainCount, &t.UserCount, &t.LDAPEnabled, &t.LDAPURL, &t.HasLogo, &t.RetentionDays, &t.MaxStorageBytes, &t.MaxUsers, &t.MaxEmails); err != nil {
			return nil, fmt.Errorf("tenantstore: scan: %w", err)
		}
		tenants = append(tenants, t)
	}
	if tenants == nil {
		tenants = []Tenant{}
	}
	return tenants, rows.Err()
}

// Get returns a single tenant by ID.
func (s *Store) Get(ctx context.Context, id int64) (*Tenant, error) {
	row := s.pool.QueryRow(ctx,
		`SELECT id, name, slug, active, created_at, (logo_data IS NOT NULL) AS has_logo FROM tenants WHERE id = $1`, id,
	)
	var t Tenant
	if err := row.Scan(&t.ID, &t.Name, &t.Slug, &t.Active, &t.CreatedAt, &t.HasLogo); err == pgx.ErrNoRows {
		return nil, fmt.Errorf("tenantstore: not found: %d", id)
	} else if err != nil {
		return nil, fmt.Errorf("tenantstore: get: %w", err)
	}
	return &t, nil
}

// SetLogo stores a logo image for a tenant (replaces any existing logo).
func (s *Store) SetLogo(ctx context.Context, tenantID int64, data []byte, contentType string) error {
	_, err := s.pool.Exec(ctx,
		`UPDATE tenants SET logo_data = $1, logo_content_type = $2 WHERE id = $3`,
		data, contentType, tenantID,
	)
	if err != nil {
		return fmt.Errorf("tenantstore: set logo: %w", err)
	}
	return nil
}

// GetLogo returns the raw logo bytes and content type for a tenant.
// Returns nil data if no logo is set.
func (s *Store) GetLogo(ctx context.Context, tenantID int64) ([]byte, string, error) {
	var data []byte
	var contentType string
	err := s.pool.QueryRow(ctx,
		`SELECT logo_data, logo_content_type FROM tenants WHERE id = $1`, tenantID,
	).Scan(&data, &contentType)
	if err == pgx.ErrNoRows {
		return nil, "", nil
	}
	if err != nil {
		return nil, "", fmt.Errorf("tenantstore: get logo: %w", err)
	}
	return data, contentType, nil
}

// DeleteLogo removes the logo from a tenant.
func (s *Store) DeleteLogo(ctx context.Context, tenantID int64) error {
	_, err := s.pool.Exec(ctx,
		`UPDATE tenants SET logo_data = NULL, logo_content_type = '' WHERE id = $1`, tenantID,
	)
	if err != nil {
		return fmt.Errorf("tenantstore: delete logo: %w", err)
	}
	return nil
}

// Update sets name and active for a tenant.
func (s *Store) Update(ctx context.Context, id int64, name string, active bool) (*Tenant, error) {
	_, err := s.pool.Exec(ctx,
		`UPDATE tenants SET name = $1, active = $2 WHERE id = $3`,
		name, active, id,
	)
	if err != nil {
		return nil, fmt.Errorf("tenantstore: update: %w", err)
	}
	return s.Get(ctx, id)
}

// Delete removes a tenant and cascades to tenant_domains and tenant_ldap.
func (s *Store) Delete(ctx context.Context, id int64) error {
	tag, err := s.pool.Exec(ctx, `DELETE FROM tenants WHERE id = $1`, id)
	if err != nil {
		return fmt.Errorf("tenantstore: delete: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("tenantstore: tenant %d not found", id)
	}
	return nil
}

// AddDomain associates a domain name with a tenant.
func (s *Store) AddDomain(ctx context.Context, tenantID int64, domain string) (*TenantDomain, error) {
	var id int64
	err := s.pool.QueryRow(ctx,
		`INSERT INTO tenant_domains (tenant_id, domain) VALUES ($1, $2) RETURNING id`,
		tenantID, domain,
	).Scan(&id)
	if err != nil {
		return nil, fmt.Errorf("tenantstore: add domain: %w", err)
	}
	return s.getDomain(ctx, id)
}

// RemoveDomain removes a domain from a tenant.
func (s *Store) RemoveDomain(ctx context.Context, tenantID, domainID int64) error {
	tag, err := s.pool.Exec(ctx,
		`DELETE FROM tenant_domains WHERE id = $1 AND tenant_id = $2`,
		domainID, tenantID,
	)
	if err != nil {
		return fmt.Errorf("tenantstore: remove domain: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("tenantstore: domain %d not found for tenant %d", domainID, tenantID)
	}
	return nil
}

// ListDomains returns all domains assigned to a tenant.
func (s *Store) ListDomains(ctx context.Context, tenantID int64) ([]TenantDomain, error) {
	rows, err := s.pool.Query(ctx,
		`SELECT id, tenant_id, domain, created_at FROM tenant_domains WHERE tenant_id = $1 ORDER BY id`,
		tenantID,
	)
	if err != nil {
		return nil, fmt.Errorf("tenantstore: list domains: %w", err)
	}
	defer rows.Close()

	var domains []TenantDomain
	for rows.Next() {
		var d TenantDomain
		if err := rows.Scan(&d.ID, &d.TenantID, &d.Domain, &d.CreatedAt); err != nil {
			return nil, fmt.Errorf("tenantstore: scan domain: %w", err)
		}
		domains = append(domains, d)
	}
	if domains == nil {
		domains = []TenantDomain{}
	}
	return domains, rows.Err()
}

// GetByDomain returns the tenant that owns the given e-mail domain.
// Used by the SMTP daemon for routing decisions.
func (s *Store) GetByDomain(ctx context.Context, domain string) (*Tenant, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT t.id, t.name, t.slug, t.active, t.created_at
		FROM tenants t
		JOIN tenant_domains td ON td.tenant_id = t.id
		WHERE td.domain = $1 AND t.active = true
		LIMIT 1
	`, domain)
	var t Tenant
	if err := row.Scan(&t.ID, &t.Name, &t.Slug, &t.Active, &t.CreatedAt); err == pgx.ErrNoRows {
		return nil, nil // no tenant for this domain is a valid state
	} else if err != nil {
		return nil, fmt.Errorf("tenantstore: get by domain: %w", err)
	}
	return &t, nil
}

// GetTenantIDByDomain returns the tenant_id for a given email domain.
// Returns nil if no tenant is found. Satisfies the auth.TenantDomainLookup interface.
func (s *Store) GetTenantIDByDomain(ctx context.Context, domain string) (*int64, error) {
	t, err := s.GetByDomain(ctx, domain)
	if err != nil {
		return nil, err
	}
	if t == nil {
		return nil, nil
	}
	id := t.ID
	return &id, nil
}

// getDomain is a private helper to load a TenantDomain by its primary key.
func (s *Store) getDomain(ctx context.Context, id int64) (*TenantDomain, error) {
	row := s.pool.QueryRow(ctx,
		`SELECT id, tenant_id, domain, created_at FROM tenant_domains WHERE id = $1`, id,
	)
	var d TenantDomain
	if err := row.Scan(&d.ID, &d.TenantID, &d.Domain, &d.CreatedAt); err != nil {
		return nil, fmt.Errorf("tenantstore: get domain: %w", err)
	}
	return &d, nil
}

// ── IMAP Mode ─────────────────────────────────────────────────────────────

// GetIMAPMode returns the imap_mode for a tenant ("personal" or "shared").
func (s *Store) GetIMAPMode(ctx context.Context, tenantID int64) (string, error) {
	var mode string
	err := s.pool.QueryRow(ctx, `SELECT imap_mode FROM tenants WHERE id = $1`, tenantID).Scan(&mode)
	if err != nil {
		return "personal", nil // safe default
	}
	return mode, nil
}

// SetIMAPMode updates the imap_mode for a tenant. Valid values: "personal", "shared".
func (s *Store) SetIMAPMode(ctx context.Context, tenantID int64, mode string) error {
	if mode != "personal" && mode != "shared" {
		return fmt.Errorf("tenantstore: invalid imap_mode %q", mode)
	}
	_, err := s.pool.Exec(ctx, `UPDATE tenants SET imap_mode = $1 WHERE id = $2`, mode, tenantID)
	return err
}

// ── Retention ─────────────────────────────────────────────────────────────────

// GetRetentionDays returns the per-tenant retention_days (0 = use global config).
func (s *Store) GetRetentionDays(ctx context.Context, tenantID int64) (int, error) {
	var days int
	err := s.pool.QueryRow(ctx, `SELECT retention_days FROM tenants WHERE id = $1`, tenantID).Scan(&days)
	if err != nil {
		return 0, nil // safe default: fall back to global
	}
	return days, nil
}

// SetRetentionDays sets the per-tenant retention_days. 0 means "use global config".
func (s *Store) SetRetentionDays(ctx context.Context, tenantID int64, days int) error {
	if days < 0 {
		return fmt.Errorf("tenantstore: retention_days must be >= 0")
	}
	tag, err := s.pool.Exec(ctx, `UPDATE tenants SET retention_days = $1 WHERE id = $2`, days, tenantID)
	if err != nil {
		return fmt.Errorf("tenantstore: set retention: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("tenantstore: tenant %d not found", tenantID)
	}
	return nil
}
