package users

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

	"github.com/jackc/pgx/v5"

	"github.com/niels/banking-app/backend/internal/domain"
)

type AuthSession struct {
	ID                  string     `json:"id"`
	UserID              string     `json:"user_id"`
	UserAgent           string     `json:"user_agent,omitempty"`
	RemoteIP            string     `json:"remote_ip,omitempty"`
	ExpiresAt           time.Time  `json:"expires_at"`
	RevokedAt           *time.Time `json:"revoked_at,omitempty"`
	RevokeReason        string     `json:"revoke_reason,omitempty"`
	ReplacedBySessionID string     `json:"replaced_by_session_id,omitempty"`
	LastUsedAt          *time.Time `json:"last_used_at,omitempty"`
	CreatedAt           time.Time  `json:"created_at"`
	UpdatedAt           time.Time  `json:"updated_at"`
}

type CreateSessionParams struct {
	UserID           string
	RefreshTokenHash string
	UserAgent        string
	RemoteIP         string
	ExpiresAt        time.Time
}

type RotatedSession struct {
	User    domain.User
	Session AuthSession
}

func (r *Repository) CreateSession(ctx context.Context, params CreateSessionParams) (AuthSession, error) {
	row := r.db.QueryRow(ctx, `
		INSERT INTO auth_sessions (user_id, refresh_token_hash, user_agent, remote_ip, expires_at, last_used_at)
		VALUES ($1, $2, NULLIF($3, ''), NULLIF($4, ''), $5, now())
		RETURNING id::text, user_id::text, COALESCE(user_agent, ''), COALESCE(remote_ip, ''),
			expires_at, revoked_at, COALESCE(revoke_reason, ''), COALESCE(replaced_by_session_id::text, ''),
			last_used_at, created_at, updated_at
	`, params.UserID, params.RefreshTokenHash, params.UserAgent, params.RemoteIP, params.ExpiresAt)
	return scanAuthSession(row)
}

func (r *Repository) RotateSession(ctx context.Context, oldRefreshTokenHash, newRefreshTokenHash, userAgent, remoteIP string, expiresAt time.Time) (RotatedSession, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return RotatedSession{}, err
	}
	defer tx.Rollback(ctx)

	var oldID, oldUserAgent, oldRemoteIP string
	var user domain.User
	err = tx.QueryRow(ctx, `
		SELECT s.id::text, COALESCE(s.user_agent, ''), COALESCE(s.remote_ip, ''),
			u.id::text, u.email, u.full_name, u.password_hash, u.role, u.created_at
		FROM auth_sessions s
		JOIN users u ON u.id = s.user_id
		WHERE s.refresh_token_hash = $1
			AND s.revoked_at IS NULL
			AND s.expires_at > now()
		FOR UPDATE OF s
	`, oldRefreshTokenHash).Scan(
		&oldID,
		&oldUserAgent,
		&oldRemoteIP,
		&user.ID,
		&user.Email,
		&user.FullName,
		&user.PasswordHash,
		&user.Role,
		&user.CreatedAt,
	)
	if errors.Is(err, pgx.ErrNoRows) {
		return RotatedSession{}, domain.ErrUnauthorized
	}
	if err != nil {
		return RotatedSession{}, err
	}

	var session AuthSession
	err = tx.QueryRow(ctx, `
		INSERT INTO auth_sessions (user_id, refresh_token_hash, user_agent, remote_ip, expires_at, last_used_at)
		VALUES ($1, $2, NULLIF($3, ''), NULLIF($4, ''), $5, now())
		RETURNING id::text, user_id::text, COALESCE(user_agent, ''), COALESCE(remote_ip, ''),
			expires_at, revoked_at, COALESCE(revoke_reason, ''), COALESCE(replaced_by_session_id::text, ''),
			last_used_at, created_at, updated_at
	`, user.ID, newRefreshTokenHash, userAgent, remoteIP, expiresAt).Scan(
		&session.ID,
		&session.UserID,
		&session.UserAgent,
		&session.RemoteIP,
		&session.ExpiresAt,
		&session.RevokedAt,
		&session.RevokeReason,
		&session.ReplacedBySessionID,
		&session.LastUsedAt,
		&session.CreatedAt,
		&session.UpdatedAt,
	)
	if err != nil {
		return RotatedSession{}, err
	}

	if _, err := tx.Exec(ctx, `
		UPDATE auth_sessions
		SET revoked_at = now(),
			revoke_reason = 'rotated',
			replaced_by_session_id = $2,
			last_used_at = now(),
			updated_at = now()
		WHERE id = $1
	`, oldID, session.ID); err != nil {
		return RotatedSession{}, err
	}
	if sessionAnomaly(oldRemoteIP, oldUserAgent, remoteIP, userAgent) {
		if _, err := tx.Exec(ctx, `
			INSERT INTO security_events (user_id, event_type, severity, identifier, remote_ip, user_agent, details)
			VALUES (
				$1, 'session_anomaly', 'high', $2, $3, NULLIF($4, ''),
				jsonb_build_object(
					'old_session_id', $5,
					'new_session_id', $6,
					'old_remote_ip', $7,
					'old_user_agent', $8,
					'new_remote_ip', $3,
					'new_user_agent', $4
				)
			)
		`, user.ID, user.Email, normalizeSessionIP(remoteIP), strings.TrimSpace(userAgent),
			oldID, session.ID, oldRemoteIP, oldUserAgent); err != nil {
			return RotatedSession{}, err
		}
	}

	if err := tx.Commit(ctx); err != nil {
		return RotatedSession{}, err
	}
	return RotatedSession{User: user, Session: session}, nil
}

func (r *Repository) RevokeSessionByRefreshTokenHash(ctx context.Context, refreshTokenHash, reason string) error {
	tag, err := r.db.Exec(ctx, `
		UPDATE auth_sessions
		SET revoked_at = now(),
			revoke_reason = NULLIF($2, ''),
			updated_at = now()
		WHERE refresh_token_hash = $1
			AND revoked_at IS NULL
	`, refreshTokenHash, reason)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return domain.ErrNotFound
	}
	return nil
}

func (r *Repository) RevokeSession(ctx context.Context, userID, sessionID, reason string) error {
	tag, err := r.db.Exec(ctx, `
		UPDATE auth_sessions
		SET revoked_at = now(),
			revoke_reason = NULLIF($3, ''),
			updated_at = now()
		WHERE id = $1
			AND user_id = $2
			AND revoked_at IS NULL
	`, sessionID, userID, reason)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return domain.ErrNotFound
	}
	return nil
}

func (r *Repository) ListSessions(ctx context.Context, userID string) ([]AuthSession, error) {
	rows, err := r.db.Query(ctx, `
		SELECT id::text, user_id::text, COALESCE(user_agent, ''), COALESCE(remote_ip, ''),
			expires_at, revoked_at, COALESCE(revoke_reason, ''), COALESCE(replaced_by_session_id::text, ''),
			last_used_at, created_at, updated_at
		FROM auth_sessions
		WHERE user_id = $1
			AND expires_at > now() - interval '7 days'
		ORDER BY created_at DESC
		LIMIT 50
	`, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	sessions := []AuthSession{}
	for rows.Next() {
		session, err := scanAuthSession(rows)
		if err != nil {
			return nil, err
		}
		sessions = append(sessions, session)
	}
	return sessions, rows.Err()
}

func scanAuthSession(row scanner) (AuthSession, error) {
	var session AuthSession
	var revokedAt sql.NullTime
	var lastUsedAt sql.NullTime
	err := row.Scan(
		&session.ID,
		&session.UserID,
		&session.UserAgent,
		&session.RemoteIP,
		&session.ExpiresAt,
		&revokedAt,
		&session.RevokeReason,
		&session.ReplacedBySessionID,
		&lastUsedAt,
		&session.CreatedAt,
		&session.UpdatedAt,
	)
	if revokedAt.Valid {
		session.RevokedAt = &revokedAt.Time
	}
	if lastUsedAt.Valid {
		session.LastUsedAt = &lastUsedAt.Time
	}
	return session, err
}

func sessionAnomaly(oldRemoteIP, oldUserAgent, newRemoteIP, newUserAgent string) bool {
	oldRemoteIP = normalizeSessionIP(oldRemoteIP)
	newRemoteIP = normalizeSessionIP(newRemoteIP)
	oldUserAgent = strings.TrimSpace(oldUserAgent)
	newUserAgent = strings.TrimSpace(newUserAgent)
	if oldRemoteIP != "" && newRemoteIP != "" && oldRemoteIP != newRemoteIP {
		return true
	}
	if oldUserAgent != "" && newUserAgent != "" && oldUserAgent != newUserAgent {
		return true
	}
	return false
}

func normalizeSessionIP(remoteIP string) string {
	remoteIP = strings.TrimSpace(remoteIP)
	if remoteIP == "" {
		return "unknown"
	}
	return remoteIP
}
