package auth

import (
	"context"
	"database/sql"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	authdomain "hyapp/services/user-service/internal/domain/auth"
)

func (r *Repository) CreateLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error {
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO login_ip_risk_jobs (
			app_code, job_id, session_id, user_id, client_ip, client_ip_hash, channel, platform, language, timezone,
			request_id, edge_country_code, status, decision, country_code, failure_reason, locked_by, locked_until_ms,
			attempt_count, created_at_ms, updated_at_ms
		)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', '', NULL, NULL, 0, ?, ?)
	`, appcode.Normalize(job.AppCode), job.JobID, job.SessionID, job.UserID, job.ClientIP, job.ClientIPHash, job.Channel, job.Platform, job.Language, job.Timezone, job.RequestID, job.EdgeCountryCode, job.Status, job.CreatedAtMs, job.UpdatedAtMs)
	return err
}

func (r *Repository) ClaimLoginIPRiskJobs(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) ([]authdomain.LoginIPRiskJob, error) {
	if batchSize <= 0 {
		batchSize = 100
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, job_id, session_id, user_id, client_ip, client_ip_hash, channel, platform, language, timezone,
		       request_id, edge_country_code, status, decision, country_code, failure_reason,
		       COALESCE(locked_by, ''), COALESCE(locked_until_ms, 0), attempt_count, created_at_ms, updated_at_ms
		FROM login_ip_risk_jobs
		WHERE app_code = ?
		  AND status IN (?, ?)
		  AND (locked_until_ms IS NULL OR locked_until_ms <= ?)
		ORDER BY updated_at_ms ASC, job_id ASC
		LIMIT ?
	`, appcode.FromContext(ctx), authdomain.LoginIPRiskStatusPending, authdomain.LoginIPRiskStatusFailed, nowMs, batchSize)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	candidates := make([]authdomain.LoginIPRiskJob, 0, batchSize)
	for rows.Next() {
		job, err := scanLoginIPRiskJob(rows)
		if err != nil {
			return nil, err
		}
		candidates = append(candidates, job)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	claimed := make([]authdomain.LoginIPRiskJob, 0, len(candidates))
	lockUntilMs := nowMs + lockTTL.Milliseconds()
	for _, job := range candidates {
		result, err := r.db.ExecContext(ctx, `
			UPDATE login_ip_risk_jobs
			SET status = ?, locked_by = ?, locked_until_ms = ?, attempt_count = attempt_count + 1, updated_at_ms = ?
			WHERE app_code = ? AND job_id = ?
			  AND status IN (?, ?)
			  AND (locked_until_ms IS NULL OR locked_until_ms <= ?)
		`, authdomain.LoginIPRiskStatusRunning, workerID, lockUntilMs, nowMs, job.AppCode, job.JobID, authdomain.LoginIPRiskStatusPending, authdomain.LoginIPRiskStatusFailed, nowMs)
		if err != nil {
			return nil, err
		}
		affected, err := result.RowsAffected()
		if err != nil {
			return nil, err
		}
		if affected == 1 {
			job.Status = authdomain.LoginIPRiskStatusRunning
			job.LockedBy = workerID
			job.LockedUntilMs = lockUntilMs
			job.AttemptCount++
			job.UpdatedAtMs = nowMs
			claimed = append(claimed, job)
		}
	}

	return claimed, nil
}

func (r *Repository) CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE login_ip_risk_jobs
		SET status = ?, decision = ?, country_code = ?, failure_reason = ?, locked_by = NULL, locked_until_ms = NULL, updated_at_ms = ?
		WHERE app_code = ? AND job_id = ?
	`, job.Status, job.Decision, job.CountryCode, job.FailureReason, job.UpdatedAtMs, appcode.Normalize(job.AppCode), job.JobID)
	return err
}

func (r *Repository) CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error {
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO login_ip_risk_decisions (
			app_code, decision_id, client_ip, client_ip_hash, decision, country_code, confidence,
			provider_attempts_json, decided_at_ms, expires_at_ms, created_at_ms
		)
		VALUES (?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?)
	`, appcode.Normalize(decision.AppCode), decision.DecisionID, decision.ClientIP, decision.ClientIPHash, decision.Decision, decision.CountryCode, decision.Confidence, decision.ProviderAttemptsJSON, decision.DecidedAtMs, decision.ExpiresAtMs, decision.CreatedAtMs)
	return err
}

func (r *Repository) ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, block_id, keyword, country_code, enabled, created_at_ms, updated_at_ms
		FROM login_risk_country_blocks
		WHERE app_code = ? AND enabled = 1
		ORDER BY country_code ASC, keyword ASC, block_id ASC
	`, appcode.FromContext(ctx))
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	blocks := make([]authdomain.LoginRiskCountryBlock, 0)
	for rows.Next() {
		var block authdomain.LoginRiskCountryBlock
		if err := rows.Scan(&block.AppCode, &block.BlockID, &block.Keyword, &block.CountryCode, &block.Enabled, &block.CreatedAtMs, &block.UpdatedAtMs); err != nil {
			return nil, err
		}
		block.CountryCode = normalizeCountryCode(block.CountryCode)
		blocks = append(blocks, block)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return blocks, nil
}

func (r *Repository) ListEnabledLoginRiskIPWhitelist(ctx context.Context) ([]authdomain.LoginRiskIPWhitelist, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, whitelist_id, ip_address, enabled, created_at_ms, updated_at_ms
		FROM login_risk_ip_whitelist
		WHERE app_code = ? AND enabled = 1
		ORDER BY ip_address ASC, whitelist_id ASC
	`, appcode.FromContext(ctx))
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]authdomain.LoginRiskIPWhitelist, 0)
	for rows.Next() {
		var item authdomain.LoginRiskIPWhitelist
		if err := rows.Scan(&item.AppCode, &item.WhitelistID, &item.IPAddress, &item.Enabled, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil {
			return nil, err
		}
		item.IPAddress = strings.TrimSpace(item.IPAddress)
		if item.IPAddress == "" {
			continue
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

func (r *Repository) ListEnabledLoginRiskUserWhitelist(ctx context.Context) ([]authdomain.LoginRiskUserWhitelist, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, whitelist_id, user_id, enabled, created_at_ms, updated_at_ms
		FROM login_risk_user_whitelist
		WHERE app_code = ? AND enabled = 1
		ORDER BY user_id ASC, whitelist_id ASC
	`, appcode.FromContext(ctx))
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]authdomain.LoginRiskUserWhitelist, 0)
	for rows.Next() {
		var item authdomain.LoginRiskUserWhitelist
		if err := rows.Scan(&item.AppCode, &item.WhitelistID, &item.UserID, &item.Enabled, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil {
			return nil, err
		}
		if item.UserID <= 0 {
			continue
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

type loginIPRiskJobScanner interface {
	Scan(dest ...any) error
}

func normalizeCountryCode(country string) string {
	country = strings.ToUpper(strings.TrimSpace(country))
	if len(country) < 2 || len(country) > 3 {
		return ""
	}
	return country
}

func scanLoginIPRiskJob(scanner loginIPRiskJobScanner) (authdomain.LoginIPRiskJob, error) {
	var job authdomain.LoginIPRiskJob
	err := scanner.Scan(
		&job.AppCode,
		&job.JobID,
		&job.SessionID,
		&job.UserID,
		&job.ClientIP,
		&job.ClientIPHash,
		&job.Channel,
		&job.Platform,
		&job.Language,
		&job.Timezone,
		&job.RequestID,
		&job.EdgeCountryCode,
		&job.Status,
		&job.Decision,
		&job.CountryCode,
		&job.FailureReason,
		&job.LockedBy,
		&job.LockedUntilMs,
		&job.AttemptCount,
		&job.CreatedAtMs,
		&job.UpdatedAtMs,
	)
	if err == sql.ErrNoRows {
		return authdomain.LoginIPRiskJob{}, err
	}
	return job, err
}
