package repository

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/warmbly/warmbly/internal/errx"
	"github.com/warmbly/warmbly/internal/infrastructure/db"
	"github.com/warmbly/warmbly/internal/models"
	"github.com/warmbly/warmbly/internal/utils/paging"
)

type APIKeyRepository interface {
	Create(ctx context.Context, orgID, userID uuid.UUID, data *models.CreateAPIKey, keyPrefix, keySuffix, keyHash string) (*models.APIKey, *errx.Error)
	GetByHash(ctx context.Context, keyHash string) (*models.APIKey, *errx.Error)
	GetByID(ctx context.Context, orgID, keyID uuid.UUID) (*models.APIKey, *errx.Error)
	List(ctx context.Context, orgID uuid.UUID, limit int, cursor *uuid.UUID) (*models.APIKeysResult, *errx.Error)
	Update(ctx context.Context, orgID, keyID uuid.UUID, data *models.UpdateAPIKey) (*models.APIKey, *errx.Error)
	Revoke(ctx context.Context, orgID, keyID uuid.UUID, reason string) *errx.Error
	UpdateLastUsed(ctx context.Context, keyID uuid.UUID, ip string) error
	LogUsage(ctx context.Context, log *models.APIKeyUsageLog) error

	// Analytics
	GetUsageSummary(ctx context.Context, orgID uuid.UUID) (*models.APIKeyUsageSummary, *errx.Error)
	GetUsageTimeseries(ctx context.Context, orgID uuid.UUID, keyID *uuid.UUID, from, to time.Time, bucket string) ([]models.APIKeyUsageBucket, *errx.Error)
	GetEndpointBreakdown(ctx context.Context, orgID uuid.UUID, keyID *uuid.UUID, from, to time.Time, limit int) ([]models.APIKeyEndpointStat, *errx.Error)
	ListUsageLogs(ctx context.Context, orgID, keyID uuid.UUID, limit int, cursor *uuid.UUID) (*models.APIKeyUsageLogsResult, *errx.Error)
}

type apiKeyRepository struct {
	DB *db.DB
}

func NewAPIKeyRepository(db *db.DB) APIKeyRepository {
	return &apiKeyRepository{DB: db}
}

const API_KEY_SELECT = `id, user_id, organization_id, name, description, key_prefix, key_suffix, permissions,
	allowed_ips, allowed_email_accounts, rate_limit_per_minute,
	status, last_used_at, last_request_ip::text, expires_at, revoked_at, revoked_reason,
	created_at, updated_at`

func scanAPIKey(row db.Scannable, key *models.APIKey) error {
	var allowedIPs, allowedAccounts []string
	err := row.Scan(
		&key.ID, &key.UserID, &key.OrganizationID, &key.Name, &key.Description, &key.KeyPrefix, &key.KeySuffix, &key.Permissions,
		&allowedIPs, &allowedAccounts, &key.RateLimitPerMinute,
		&key.Status, &key.LastUsedAt, &key.LastRequestIP, &key.ExpiresAt, &key.RevokedAt, &key.RevokedReason,
		&key.CreatedAt, &key.UpdatedAt,
	)
	if err != nil {
		return err
	}
	key.AllowedIPs = allowedIPs
	key.AllowedEmailAccounts = make([]uuid.UUID, 0, len(allowedAccounts))
	for _, acc := range allowedAccounts {
		if id, err := uuid.Parse(acc); err == nil {
			key.AllowedEmailAccounts = append(key.AllowedEmailAccounts, id)
		}
	}
	return nil
}

func (r *apiKeyRepository) Create(ctx context.Context, orgID, userID uuid.UUID, data *models.CreateAPIKey, keyPrefix, keySuffix, keyHash string) (*models.APIKey, *errx.Error) {
	query := fmt.Sprintf(`
		INSERT INTO api_keys (user_id, organization_id, name, description, key_prefix, key_suffix, key_hash, permissions, allowed_ips, allowed_email_accounts, rate_limit_per_minute, expires_at)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, COALESCE($11, 60), $12)
		RETURNING %s
	`, API_KEY_SELECT)

	var allowedAccountsStr []string
	for _, acc := range data.AllowedEmailAccounts {
		allowedAccountsStr = append(allowedAccountsStr, acc.String())
	}

	params := []any{
		userID,
		orgID,
		data.Name,
		data.Description,
		keyPrefix,
		keySuffix,
		keyHash,
		data.Permissions,
		data.AllowedIPs,
		allowedAccountsStr,
		data.RateLimitPerMinute,
		data.ExpiresAt,
	}

	var key models.APIKey
	row := r.DB.QueryRow(ctx, query, params...)
	if err := scanAPIKey(row, &key); err != nil {
		db.CaptureError(err, query, params, "queryrow")
		return nil, errx.InternalError()
	}

	return &key, nil
}

func (r *apiKeyRepository) GetByHash(ctx context.Context, keyHash string) (*models.APIKey, *errx.Error) {
	query := fmt.Sprintf(`
		SELECT %s FROM api_keys
		WHERE key_hash = $1 AND status = 'active'
	`, API_KEY_SELECT)

	var key models.APIKey
	row := r.DB.QueryRow(ctx, query, keyHash)
	if err := scanAPIKey(row, &key); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errx.ErrAuth
		}
		db.CaptureError(err, query, []any{keyHash}, "queryrow")
		return nil, errx.InternalError()
	}

	// Check if expired
	if key.ExpiresAt != nil && key.ExpiresAt.Before(time.Now()) {
		return nil, errx.ErrAuth
	}

	return &key, nil
}

func (r *apiKeyRepository) GetByID(ctx context.Context, orgID, keyID uuid.UUID) (*models.APIKey, *errx.Error) {
	query := fmt.Sprintf(`
		SELECT %s FROM api_keys
		WHERE organization_id = $1 AND id = $2
	`, API_KEY_SELECT)

	params := []any{orgID, keyID}

	var key models.APIKey
	row := r.DB.QueryRow(ctx, query, params...)
	if err := scanAPIKey(row, &key); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errx.ErrNotFound
		}
		db.CaptureError(err, query, params, "queryrow")
		return nil, errx.InternalError()
	}

	return &key, nil
}

func (r *apiKeyRepository) List(ctx context.Context, orgID uuid.UUID, limit int, cursor *uuid.UUID) (*models.APIKeysResult, *errx.Error) {
	query := fmt.Sprintf(`
		SELECT %s FROM api_keys
		WHERE organization_id = $1
		  AND ($2::uuid IS NULL OR (created_at, id) < (
			SELECT created_at, id FROM api_keys WHERE id = $2
		  ))
		ORDER BY created_at DESC, id DESC
		LIMIT $3
	`, API_KEY_SELECT)

	params := []any{orgID, cursor, limit + 1}

	rows, err := r.DB.Query(ctx, query, params...)
	if err != nil {
		db.CaptureError(err, query, params, "query")
		return nil, errx.InternalError()
	}
	defer rows.Close()

	keys := make([]models.APIKey, 0, limit)
	for rows.Next() {
		var key models.APIKey
		if err := scanAPIKey(rows, &key); err != nil {
			db.CaptureError(err, "", nil, "scan")
			return nil, errx.InternalError()
		}
		keys = append(keys, key)
	}

	var nextCursor *string
	hasMore := false
	if len(keys) > limit {
		hasMore = true
		nextCursor = paging.EncodeUUID(keys[limit].ID)
		keys = keys[:limit]
	}

	return &models.APIKeysResult{
		Data: keys,
		Pagination: models.Pagination{
			NextCursor: nextCursor,
			HasMore:    hasMore,
		},
	}, nil
}

func (r *apiKeyRepository) Update(ctx context.Context, orgID, keyID uuid.UUID, data *models.UpdateAPIKey) (*models.APIKey, *errx.Error) {
	setClauses := []string{}
	args := []any{orgID, keyID}
	argPos := 3

	if data.Name != nil {
		setClauses = append(setClauses, fmt.Sprintf("name = $%d", argPos))
		args = append(args, *data.Name)
		argPos++
	}
	if data.Description != nil {
		setClauses = append(setClauses, fmt.Sprintf("description = $%d", argPos))
		args = append(args, *data.Description)
		argPos++
	}
	if data.Permissions != nil {
		setClauses = append(setClauses, fmt.Sprintf("permissions = $%d", argPos))
		args = append(args, *data.Permissions)
		argPos++
	}
	if data.AllowedIPs != nil {
		setClauses = append(setClauses, fmt.Sprintf("allowed_ips = $%d", argPos))
		args = append(args, data.AllowedIPs)
		argPos++
	}
	if data.AllowedEmailAccounts != nil {
		var allowedAccountsStr []string
		for _, acc := range data.AllowedEmailAccounts {
			allowedAccountsStr = append(allowedAccountsStr, acc.String())
		}
		setClauses = append(setClauses, fmt.Sprintf("allowed_email_accounts = $%d", argPos))
		args = append(args, allowedAccountsStr)
		argPos++
	}
	if data.RateLimitPerMinute != nil {
		setClauses = append(setClauses, fmt.Sprintf("rate_limit_per_minute = $%d", argPos))
		args = append(args, *data.RateLimitPerMinute)
		argPos++
	}

	if len(setClauses) == 0 {
		return nil, errx.ErrNotEnough
	}

	setClauses = append(setClauses, "updated_at = now()")

	query := fmt.Sprintf(`
		UPDATE api_keys SET %s
		WHERE organization_id = $1 AND id = $2 AND status = 'active'
		RETURNING %s
	`, strings.Join(setClauses, ", "), API_KEY_SELECT)

	var key models.APIKey
	row := r.DB.QueryRow(ctx, query, args...)
	if err := scanAPIKey(row, &key); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errx.ErrNotFound
		}
		db.CaptureError(err, query, args, "queryrow")
		return nil, errx.InternalError()
	}

	return &key, nil
}

func (r *apiKeyRepository) Revoke(ctx context.Context, orgID, keyID uuid.UUID, reason string) *errx.Error {
	query := `
		UPDATE api_keys
		SET status = 'revoked', revoked_at = now(), revoked_reason = $3, updated_at = now()
		WHERE organization_id = $1 AND id = $2 AND status = 'active'
	`

	params := []any{orgID, keyID, reason}

	cmd, err := r.DB.Exec(ctx, query, params...)
	if err != nil {
		db.CaptureError(err, query, params, "exec")
		return errx.InternalError()
	}

	if cmd.RowsAffected() == 0 {
		return errx.ErrNotFound
	}

	return nil
}

func (r *apiKeyRepository) UpdateLastUsed(ctx context.Context, keyID uuid.UUID, ip string) error {
	// Casting via NULLIF lets the same query handle "no IP available" (worker
	// background calls, tests) without erroring on an empty INET.
	query := `UPDATE api_keys
		SET last_used_at = now(),
		    last_request_ip = NULLIF($2, '')::inet
		WHERE id = $1`
	_, err := r.DB.Exec(ctx, query, keyID, ip)
	return err
}

func (r *apiKeyRepository) LogUsage(ctx context.Context, log *models.APIKeyUsageLog) error {
	query := `
		INSERT INTO api_key_usage_logs (api_key_id, endpoint, method, ip_address, user_agent, response_status, response_time_ms)
		VALUES ($1, $2, $3, $4, $5, $6, $7)
	`

	params := []any{
		log.APIKeyID,
		log.Endpoint,
		log.Method,
		log.IPAddress,
		log.UserAgent,
		log.ResponseCode,
		log.ResponseTime,
	}

	_, err := r.DB.Exec(ctx, query, params...)
	if err != nil {
		db.CaptureError(err, query, params, "exec")
	}
	return err
}

// GetUsageSummary returns the org-level overview shown in the dashboard
// strip. Counts are over the last 24 hours so the page can show "what's
// going on right now" without needing a date picker.
func (r *apiKeyRepository) GetUsageSummary(ctx context.Context, orgID uuid.UUID) (*models.APIKeyUsageSummary, *errx.Error) {
	query := `
		WITH key_counts AS (
			SELECT
				COUNT(*) FILTER (WHERE status = 'active')  AS active_keys,
				COUNT(*) FILTER (WHERE status = 'revoked') AS revoked_keys,
				COUNT(*) FILTER (WHERE status = 'expired') AS expired_keys
			FROM api_keys
			WHERE organization_id = $1
		),
		usage AS (
			SELECT
				COUNT(*) AS total,
				COUNT(*) FILTER (WHERE l.response_status >= 400) AS errors,
				COALESCE(AVG(l.response_time_ms), 0)::float8 AS avg_latency_ms,
				MAX(l.created_at) AS last_call_at
			FROM api_key_usage_logs l
			JOIN api_keys k ON k.id = l.api_key_id
			WHERE k.organization_id = $1
			  AND l.created_at >= now() - INTERVAL '24 hours'
		)
		SELECT
			key_counts.active_keys,
			key_counts.revoked_keys,
			key_counts.expired_keys,
			usage.total,
			usage.errors,
			usage.avg_latency_ms,
			usage.last_call_at
		FROM key_counts CROSS JOIN usage
	`

	var s models.APIKeyUsageSummary
	row := r.DB.QueryRow(ctx, query, orgID)
	if err := row.Scan(
		&s.ActiveKeys, &s.RevokedKeys, &s.ExpiredKeys,
		&s.Requests24h, &s.Errors24h, &s.AvgLatencyMs24h, &s.LastCallAt,
	); err != nil {
		db.CaptureError(err, query, []any{orgID}, "queryrow")
		return nil, errx.InternalError()
	}
	return &s, nil
}

// GetUsageTimeseries returns per-bucket request counts split by status
// family (success / 4xx / 5xx). Bucket is one of "minute", "hour", "day".
// keyID is optional; nil means "across every key in the org".
func (r *apiKeyRepository) GetUsageTimeseries(ctx context.Context, orgID uuid.UUID, keyID *uuid.UUID, from, to time.Time, bucket string) ([]models.APIKeyUsageBucket, *errx.Error) {
	// Whitelist the trunc unit so we can interpolate it without opening up
	// a SQL injection through the bucket parameter.
	truncUnit := "hour"
	switch bucket {
	case "minute":
		truncUnit = "minute"
	case "hour":
		truncUnit = "hour"
	case "day":
		truncUnit = "day"
	}

	query := fmt.Sprintf(`
		SELECT
			date_trunc('%s', l.created_at) AS bucket,
			COUNT(*) AS total,
			COUNT(*) FILTER (WHERE l.response_status BETWEEN 200 AND 399) AS success,
			COUNT(*) FILTER (WHERE l.response_status BETWEEN 400 AND 499) AS client_errors,
			COUNT(*) FILTER (WHERE l.response_status >= 500) AS server_errors,
			COALESCE(AVG(l.response_time_ms), 0)::float8 AS avg_latency_ms
		FROM api_key_usage_logs l
		JOIN api_keys k ON k.id = l.api_key_id
		WHERE k.organization_id = $1
		  AND ($2::uuid IS NULL OR l.api_key_id = $2)
		  AND l.created_at >= $3
		  AND l.created_at < $4
		GROUP BY bucket
		ORDER BY bucket ASC
	`, truncUnit)

	params := []any{orgID, keyID, from, to}
	rows, err := r.DB.Query(ctx, query, params...)
	if err != nil {
		db.CaptureError(err, query, params, "query")
		return nil, errx.InternalError()
	}
	defer rows.Close()

	out := make([]models.APIKeyUsageBucket, 0, 64)
	for rows.Next() {
		var b models.APIKeyUsageBucket
		if err := rows.Scan(&b.Bucket, &b.Total, &b.Success, &b.ClientErrors, &b.ServerErrors, &b.AvgLatencyMs); err != nil {
			db.CaptureError(err, query, params, "scan")
			return nil, errx.InternalError()
		}
		out = append(out, b)
	}
	return out, nil
}

// GetEndpointBreakdown returns the top `limit` endpoints by call count for
// the given key (or the whole org if keyID is nil) over [from, to).
func (r *apiKeyRepository) GetEndpointBreakdown(ctx context.Context, orgID uuid.UUID, keyID *uuid.UUID, from, to time.Time, limit int) ([]models.APIKeyEndpointStat, *errx.Error) {
	if limit <= 0 || limit > 100 {
		limit = 25
	}
	query := `
		SELECT
			l.endpoint,
			l.method,
			COUNT(*) AS count,
			COUNT(*) FILTER (WHERE l.response_status >= 400) AS error_count,
			COALESCE(AVG(l.response_time_ms), 0)::float8 AS avg_latency_ms
		FROM api_key_usage_logs l
		JOIN api_keys k ON k.id = l.api_key_id
		WHERE k.organization_id = $1
		  AND ($2::uuid IS NULL OR l.api_key_id = $2)
		  AND l.created_at >= $3
		  AND l.created_at < $4
		GROUP BY l.endpoint, l.method
		ORDER BY count DESC
		LIMIT $5
	`

	params := []any{orgID, keyID, from, to, limit}
	rows, err := r.DB.Query(ctx, query, params...)
	if err != nil {
		db.CaptureError(err, query, params, "query")
		return nil, errx.InternalError()
	}
	defer rows.Close()

	out := make([]models.APIKeyEndpointStat, 0, limit)
	for rows.Next() {
		var s models.APIKeyEndpointStat
		if err := rows.Scan(&s.Endpoint, &s.Method, &s.Count, &s.ErrorCount, &s.AvgLatencyMs); err != nil {
			db.CaptureError(err, query, params, "scan")
			return nil, errx.InternalError()
		}
		out = append(out, s)
	}
	return out, nil
}

// ListUsageLogs returns recent raw request entries for a single key. Used
// to power the live activity table in the detail drawer.
func (r *apiKeyRepository) ListUsageLogs(ctx context.Context, orgID, keyID uuid.UUID, limit int, cursor *uuid.UUID) (*models.APIKeyUsageLogsResult, *errx.Error) {
	if limit <= 0 || limit > 200 {
		limit = 50
	}

	query := `
		SELECT
			l.id, l.api_key_id, l.endpoint, l.method, l.ip_address::text,
			COALESCE(l.user_agent, ''), COALESCE(l.response_status, 0),
			COALESCE(l.response_time_ms, 0), l.created_at
		FROM api_key_usage_logs l
		JOIN api_keys k ON k.id = l.api_key_id
		WHERE k.organization_id = $1
		  AND l.api_key_id = $2
		  AND ($3::uuid IS NULL OR (l.created_at, l.id) < (
			SELECT created_at, id FROM api_key_usage_logs WHERE id = $3
		  ))
		ORDER BY l.created_at DESC, l.id DESC
		LIMIT $4
	`

	params := []any{orgID, keyID, cursor, limit + 1}
	rows, err := r.DB.Query(ctx, query, params...)
	if err != nil {
		db.CaptureError(err, query, params, "query")
		return nil, errx.InternalError()
	}
	defer rows.Close()

	logs := make([]models.APIKeyUsageLog, 0, limit)
	for rows.Next() {
		var l models.APIKeyUsageLog
		if err := rows.Scan(&l.ID, &l.APIKeyID, &l.Endpoint, &l.Method, &l.IPAddress, &l.UserAgent, &l.ResponseCode, &l.ResponseTime, &l.CreatedAt); err != nil {
			db.CaptureError(err, query, params, "scan")
			return nil, errx.InternalError()
		}
		logs = append(logs, l)
	}

	var nextCursor *string
	hasMore := false
	if len(logs) > limit {
		hasMore = true
		nextCursor = paging.EncodeUUID(logs[limit].ID)
		logs = logs[:limit]
	}

	return &models.APIKeyUsageLogsResult{
		Data: logs,
		Pagination: models.Pagination{
			NextCursor: nextCursor,
			HasMore:    hasMore,
		},
	}, nil
}
