<?php

declare(strict_types=1);

require_once dirname(__DIR__, 2) . '/core/auth.php';
require_once dirname(__DIR__, 2) . '/core/csrf.php';
require_once dirname(__DIR__, 2) . '/core/validation.php';
require_once dirname(__DIR__, 2) . '/core/audit.php';
require_once dirname(__DIR__, 2) . '/modules/clubs/helpers.php';
require_once dirname(__DIR__, 2) . '/modules/players/helpers.php';
require_once dirname(__DIR__, 2) . '/modules/notifications/helpers.php';

function transfer_module_boot(): void
{
    transfer_sync_window_statuses();

    require_login();

    if (!can_manage_transfers() && !can_approve_transfers()) {
        abort_request(403, 'Access denied', 'You do not have permission to access transfer management.');
    }
}

function can_manage_transfers(): bool
{
    return can('manage_transfers');
}

function can_approve_transfers(): bool
{
    return can('approve_transfers');
}

function can_manage_transfer_windows(): bool
{
    return can_manage_transfers() && !is_club_admin();
}

function transfer_stage_label(string $stage): string
{
    return match ($stage) {
        'interest_registered' => 'Interest registered',
        'club_review' => 'Club review',
        'verbal_agreement' => 'Verbal agreement',
        'awaiting_club_approval' => 'Awaiting club approval',
        'awaiting_league_approval' => 'Awaiting league approval',
        'approved' => 'Approved',
        default => ucwords(str_replace('_', ' ', $stage)),
    };
}

function transfer_stage_badge_class(string $stage): string
{
    return match ($stage) {
        'completed' => 'badge-soft-success',
        'rejected', 'cancelled' => 'badge-soft-danger',
        'awaiting_league_approval', 'awaiting_club_approval', 'club_review', 'negotiation', 'verbal_agreement', 'medical', 'approved' => 'bg-warning-subtle text-warning-emphasis',
        default => 'text-bg-light',
    };
}

function transfer_scope_conditions(array &$params): string
{
    if (!is_club_admin()) {
        return '';
    }

    $clubId = user_club_id();
    if ($clubId === null) {
        return ' WHERE 1 = 0';
    }

    $params['club_id'] = $clubId;

    return ' WHERE (t.selling_club_id = :club_id OR t.buying_club_id = :club_id)';
}

function transfer_summary_cards(): array
{
    $params = [];
    $where = transfer_scope_conditions($params);

    return [
        'open_windows' => (int) db_fetch_value('SELECT COUNT(*) FROM transfer_windows WHERE status = "open"', [], 0),
        'active_deals' => (int) db_fetch_value('SELECT COUNT(*) FROM transfers t' . ($where === '' ? ' WHERE t.current_stage NOT IN ("completed", "rejected", "cancelled")' : $where . ' AND t.current_stage NOT IN ("completed", "rejected", "cancelled")'), $params, 0),
        'pending_approvals' => (int) db_fetch_value(
            'SELECT COUNT(*)
             FROM transfer_approvals ta
             INNER JOIN transfers t ON t.id = ta.transfer_id' .
                ($where === '' ? ' WHERE ta.status = "pending"' : $where . ' AND ta.status = "pending"'),
            $params,
            0
        ),
        'unread_notifications' => (int) db_fetch_value(
            'SELECT COUNT(*) FROM transfer_notifications tn WHERE tn.recipient_user_id = :user_id AND tn.is_read = 0',
            ['user_id' => (int) ($_SESSION['user_id'] ?? 0)],
            0
        ),
    ];
}

function transfer_recent_list(): array
{
    $params = [];
    $where = transfer_scope_conditions($params);

    return db_fetch_all(
        'SELECT t.*, p.first_name, p.last_name,
                selling.name AS selling_club_name,
                buying.name AS buying_club_name,
                u.full_name AS initiated_by_name
         FROM transfers t
         INNER JOIN players p ON p.id = t.player_id
         INNER JOIN clubs selling ON selling.id = t.selling_club_id
         INNER JOIN clubs buying ON buying.id = t.buying_club_id
         INNER JOIN users u ON u.id = t.initiated_by_user_id' .
            $where .
            ' ORDER BY t.updated_at DESC, t.id DESC
         LIMIT 25',
        $params
    );
}

function transfer_window_list(): array
{
    return db_fetch_all(
        'SELECT tw.*, s.name AS season_name
         FROM transfer_windows tw
         LEFT JOIN seasons s ON s.id = tw.season_id
         ORDER BY tw.start_date DESC, tw.id DESC'
    );
}

function transfer_window_report_list(): array
{
    return db_fetch_all(
        'SELECT tw.*, s.name AS season_name,
                (SELECT COUNT(*) FROM transfers t WHERE t.window_id = tw.id) AS total_transfers,
                (SELECT COUNT(*) FROM transfers t WHERE t.window_id = tw.id AND t.current_stage = "completed") AS completed_transfers,
                (SELECT COUNT(*) FROM transfers t WHERE t.window_id = tw.id AND t.current_stage IN ("rejected", "cancelled")) AS failed_transfers,
                (SELECT COUNT(*) FROM transfers t WHERE t.window_id = tw.id AND t.current_stage NOT IN ("completed", "rejected", "cancelled")) AS active_transfers
         FROM transfer_windows tw
         LEFT JOIN seasons s ON s.id = tw.season_id
         ORDER BY tw.start_date DESC, tw.id DESC'
    );
}

function transfer_window_report_summary(): array
{
    return [
        'total_windows' => (int) db_fetch_value('SELECT COUNT(*) FROM transfer_windows', [], 0),
        'open_windows' => (int) db_fetch_value('SELECT COUNT(*) FROM transfer_windows WHERE status = "open"', [], 0),
        'closed_windows' => (int) db_fetch_value('SELECT COUNT(*) FROM transfer_windows WHERE status = "closed"', [], 0),
        'draft_windows' => (int) db_fetch_value('SELECT COUNT(*) FROM transfer_windows WHERE status = "draft"', [], 0),
    ];
}

function transfer_season_options(): array
{
    return db_fetch_all('SELECT id, name, status FROM seasons ORDER BY start_date DESC, id DESC');
}

function transfer_sync_window_statuses(): void
{
    $expiredWindows = db_fetch_all(
        'SELECT tw.*, s.name AS season_name
         FROM transfer_windows tw
         LEFT JOIN seasons s ON s.id = tw.season_id
         WHERE tw.status = "open"
           AND tw.end_date < CURDATE()'
    );

    foreach ($expiredWindows as $window) {
        transfer_apply_window_status((int) $window['id'], 'closed', $window, true);
    }
}

function transfer_open_window(): ?array
{
    return db_fetch_one(
        'SELECT tw.*, s.name AS season_name
         FROM transfer_windows tw
         LEFT JOIN seasons s ON s.id = tw.season_id
         WHERE tw.status = "open"
         ORDER BY tw.start_date ASC, tw.id ASC
         LIMIT 1'
    );
}

function transfer_window_countdown(?array $window): ?array
{
    if ($window === null) {
        return null;
    }

    $deadline = $window['end_date'] . ' 23:59:59';
    $secondsRemaining = max(0, strtotime($deadline) - time());

    return [
        'deadline_iso' => date('c', strtotime($deadline)),
        'seconds_remaining' => $secondsRemaining,
        'days_remaining' => (int) floor($secondsRemaining / 86400),
    ];
}

function transfer_active_window_snapshot(): array
{
    transfer_sync_window_statuses();

    $window = transfer_open_window();

    return [
        'window' => $window,
        'countdown' => transfer_window_countdown($window),
    ];
}

function transfer_window_status_badge_class(string $status): string
{
    return match ($status) {
        'open' => 'badge-soft-success',
        'closed' => 'badge-soft-danger',
        default => 'text-bg-light',
    };
}

function validate_transfer_window_payload(array $data, array &$errors): array
{
    if (!can_manage_transfer_windows()) {
        $errors['window'] = 'You do not have permission to manage transfer windows.';
    }

    validate_required($data, [
        'name' => 'Window name',
        'start_date' => 'Start date',
        'end_date' => 'End date',
    ], $errors);
    validate_date_field($data, 'start_date', 'Start date', $errors);
    validate_date_field($data, 'end_date', 'End date', $errors);

    $status = trim((string) ($data['status'] ?? 'draft'));
    if (!in_array($status, ['draft', 'open'], true)) {
        $errors['status'] = 'Window status selection is invalid.';
    }

    $seasonId = (int) ($data['season_id'] ?? 0);
    if ($seasonId > 0 && (int) db_fetch_value('SELECT COUNT(*) FROM seasons WHERE id = :id', ['id' => $seasonId], 0) === 0) {
        $errors['season_id'] = 'Season selection is invalid.';
    }

    $name = trim((string) ($data['name'] ?? ''));
    $startDate = trim((string) ($data['start_date'] ?? ''));
    $endDate = trim((string) ($data['end_date'] ?? ''));
    validate_date_order($startDate, $endDate, 'Start date', 'End date', $errors);

    if ($status === 'open' && transfer_open_window() !== null) {
        $errors['status'] = 'Close the current open transfer window before opening another one.';
    }

    return [
        'season_id' => $seasonId > 0 ? $seasonId : null,
        'name' => $name,
        'start_date' => $startDate,
        'end_date' => $endDate,
        'status' => $status,
    ];
}

function transfer_window_news_copy(array $window, string $status, bool $automatic = false): array
{
    if ($status === 'open') {
        return [
            'headline' => sprintf('Transfer window open: %s', $window['name'] ?? 'Transfer window'),
            'body' => sprintf(
                '%s is now open%s and will run from %s until %s. Clubs can begin transfer activity immediately.',
                $window['name'] ?? 'A transfer window',
                !empty($window['season_name']) ? ' for ' . $window['season_name'] : '',
                $window['start_date'] ?? 'today',
                $window['end_date'] ?? 'the deadline'
            ),
        ];
    }

    return [
        'headline' => sprintf('Transfer window closed: %s', $window['name'] ?? 'Transfer window'),
        'body' => sprintf(
            '%s has been closed%s. %s',
            $window['name'] ?? 'The transfer window',
            !empty($window['season_name']) ? ' for ' . $window['season_name'] : '',
            $automatic
                ? 'The deadline was reached, so new transfer activity is now blocked automatically.'
                : 'League management has ended this transfer window, so new transfer activity is now blocked.'
        ),
    ];
}

function transfer_apply_window_status(int $windowId, string $status, ?array $window = null, bool $automatic = false): void
{
    $window ??= db_fetch_one(
        'SELECT tw.*, s.name AS season_name
         FROM transfer_windows tw
         LEFT JOIN seasons s ON s.id = tw.season_id
         WHERE tw.id = :id
         LIMIT 1',
        ['id' => $windowId]
    );

    if ($window === null) {
        throw new RuntimeException('The requested transfer window does not exist.');
    }

    if ((string) $window['status'] === $status) {
        return;
    }

    if ($status === 'open' && strtotime((string) $window['end_date']) < strtotime(date('Y-m-d'))) {
        throw new RuntimeException('Expired transfer windows cannot be opened.');
    }

    db()->beginTransaction();

    try {
        if ($status === 'open') {
            db_query('UPDATE transfer_windows SET status = "closed", updated_at = NOW() WHERE status = "open" AND id <> :id', ['id' => $windowId]);
        }

        db_query(
            'UPDATE transfer_windows
             SET status = :status,
                 updated_at = NOW()
             WHERE id = :id',
            [
                'status' => $status,
                'id' => $windowId,
            ]
        );

        $news = transfer_window_news_copy($window, $status, $automatic);
        $newsPostId = transfer_publish_news($news['headline'], $news['body']);

        audit_log(
            $automatic ? 'auto_set_transfer_window_status' : 'set_transfer_window_status',
            'transfer_windows',
            $windowId,
            $window,
            ['status' => $status, 'automatic' => $automatic, 'news_post_id' => $newsPostId]
        );

        db()->commit();
    } catch (Throwable $exception) {
        if (db()->inTransaction()) {
            db()->rollBack();
        }

        throw $exception;
    }
}

function transfer_create_window(array $payload): int
{
    db()->beginTransaction();

    try {
        if ($payload['status'] === 'open') {
            db_query('UPDATE transfer_windows SET status = "closed", updated_at = NOW() WHERE status = "open"');
        }

        db_query(
            'INSERT INTO transfer_windows (season_id, name, start_date, end_date, status, created_at, updated_at)
             VALUES (:season_id, :name, :start_date, :end_date, :status, NOW(), NOW())',
            $payload
        );

        $windowId = (int) db()->lastInsertId();

        if ($payload['status'] === 'open') {
            $window = $payload + ['id' => $windowId, 'season_name' => null];
            $news = transfer_window_news_copy($window, 'open');
            $newsPostId = transfer_publish_news($news['headline'], $news['body']);
        } else {
            $newsPostId = null;
        }

        audit_log('create_transfer_window', 'transfer_windows', $windowId, null, $payload + ['news_post_id' => $newsPostId]);
        db()->commit();

        return $windowId;
    } catch (Throwable $exception) {
        if (db()->inTransaction()) {
            db()->rollBack();
        }

        throw $exception;
    }
}

function transfer_set_window_status(int $windowId, string $status): void
{
    if (!can_manage_transfer_windows()) {
        abort_request(403, 'Access denied', 'You do not have permission to manage transfer windows.');
    }

    $window = db_fetch_one(
        'SELECT tw.*, s.name AS season_name
         FROM transfer_windows tw
         LEFT JOIN seasons s ON s.id = tw.season_id
         WHERE tw.id = :id
         LIMIT 1',
        ['id' => $windowId]
    );
    if ($window === null) {
        abort_request(404, 'Transfer window not found', 'The requested transfer window does not exist.');
    }

    transfer_apply_window_status($windowId, $status, $window, false);
}

function transfer_market_filters_from_request(): array
{
    return [
        'q' => trim((string) ($_GET['q'] ?? '')),
        'position' => trim((string) ($_GET['position'] ?? '')),
        'club_id' => (int) ($_GET['club_id'] ?? 0),
    ];
}

function transfer_market_player_list(array $filters): array
{
    $params = [];
    $conditions = [
        'p.status = "active"',
        'p.transfer_status IN ("active", "free_agent")',
        'c.status = "active"',
    ];

    $viewerClubId = is_club_admin() ? user_club_id() : null;
    if ($viewerClubId !== null) {
        $conditions[] = 'p.club_id <> :viewer_club_id';
        $params['viewer_club_id'] = $viewerClubId;
    }

    if ($filters['q'] !== '') {
        $conditions[] = '(p.first_name LIKE :search OR p.last_name LIKE :search OR c.name LIKE :search OR p.registration_number LIKE :search OR p.nin LIKE :search)';
        $params['search'] = '%' . $filters['q'] . '%';
    }

    if ($filters['position'] !== '' && in_array($filters['position'], player_position_options(), true)) {
        $conditions[] = 'p.position = :position';
        $params['position'] = $filters['position'];
    }

    if (!is_club_admin() && $filters['club_id'] > 0) {
        $conditions[] = 'p.club_id = :club_id';
        $params['club_id'] = $filters['club_id'];
    }

    $buyerClubId = $viewerClubId;

    return db_fetch_all(
        'SELECT p.*, c.name AS club_name, c.short_name,
                ' . ($buyerClubId !== null
            ? '(SELECT COUNT(*)
                        FROM transfers t
                        WHERE t.player_id = p.id
                          AND t.buying_club_id = ' . (int) $buyerClubId . '
                          AND t.current_stage NOT IN ("completed", "rejected", "cancelled"))'
            : '0') . ' AS active_interest_count
         FROM players p
         INNER JOIN clubs c ON c.id = p.club_id
         WHERE ' . implode(' AND ', $conditions) . '
         ORDER BY c.name ASC, p.last_name ASC, p.first_name ASC',
        $params
    );
}

function get_transfer_market_player_by_id(int $playerId): ?array
{
    $player = db_fetch_one(
        'SELECT p.*, c.name AS club_name, c.short_name
         FROM players p
         INNER JOIN clubs c ON c.id = p.club_id
         WHERE p.id = :id
           AND p.status = "active"
           AND p.transfer_status IN ("active", "free_agent")
           AND c.status = "active"
         LIMIT 1',
        ['id' => $playerId]
    );

    if ($player === null) {
        return null;
    }

    if (is_club_admin() && user_club_id() === (int) $player['club_id']) {
        return null;
    }

    return $player;
}

function find_transfer_market_player_or_abort(int $playerId): array
{
    $player = get_transfer_market_player_by_id($playerId);

    if ($player === null) {
        abort_request(404, 'Market player not found', 'The selected player is not available in the transfer market.');
    }

    return $player;
}

function transfer_buyer_club_options(): array
{
    if (is_club_admin()) {
        return club_options();
    }

    return db_fetch_all('SELECT id, name, short_name, status FROM clubs WHERE status = "active" ORDER BY name ASC');
}

function transfer_notification_list(): array
{
    return db_fetch_all(
        'SELECT tn.*, t.current_stage,
                p.first_name, p.last_name,
                selling.name AS selling_club_name,
                buying.name AS buying_club_name
         FROM transfer_notifications tn
         INNER JOIN transfers t ON t.id = tn.transfer_id
         INNER JOIN players p ON p.id = t.player_id
         INNER JOIN clubs selling ON selling.id = t.selling_club_id
         INNER JOIN clubs buying ON buying.id = t.buying_club_id
         WHERE tn.recipient_user_id = :user_id
         ORDER BY tn.is_read ASC, tn.created_at DESC, tn.id DESC
         LIMIT 10',
        ['user_id' => (int) ($_SESSION['user_id'] ?? 0)]
    );
}

function transfer_mark_notifications_read(int $transferId, int $recipientUserId): void
{
    db_query(
        'UPDATE transfer_notifications
         SET is_read = 1,
             read_at = NOW()
         WHERE transfer_id = :transfer_id
           AND recipient_user_id = :recipient_user_id
           AND is_read = 0',
        [
            'transfer_id' => $transferId,
            'recipient_user_id' => $recipientUserId,
        ]
    );
}

function get_transfer_by_id(int $transferId): ?array
{
    $params = ['id' => $transferId];
    $scopeSql = '';

    if (is_club_admin()) {
        $clubId = user_club_id();

        if ($clubId === null) {
            return null;
        }

        $params['club_id'] = $clubId;
        $scopeSql = ' AND (t.selling_club_id = :club_id OR t.buying_club_id = :club_id)';
    }

    return db_fetch_one(
        'SELECT t.*, p.first_name, p.last_name, p.position, p.registration_number, p.nin,
                selling.name AS selling_club_name,
                buying.name AS buying_club_name,
                u.full_name AS initiated_by_name,
                tw.name AS window_name
         FROM transfers t
         INNER JOIN players p ON p.id = t.player_id
         INNER JOIN clubs selling ON selling.id = t.selling_club_id
         INNER JOIN clubs buying ON buying.id = t.buying_club_id
         INNER JOIN users u ON u.id = t.initiated_by_user_id
         LEFT JOIN transfer_windows tw ON tw.id = t.window_id
         WHERE t.id = :id' . $scopeSql . '
         LIMIT 1',
        $params
    );
}

function find_transfer_or_abort(int $transferId): array
{
    $transfer = get_transfer_by_id($transferId);

    if ($transfer === null) {
        abort_request(404, 'Transfer not found', 'The requested transfer record does not exist or is outside your access scope.');
    }

    return $transfer;
}

function transfer_timeline_list(int $transferId): array
{
    return db_fetch_all(
        'SELECT tte.*, u.full_name AS created_by_name
         FROM transfer_timeline_events tte
         LEFT JOIN users u ON u.id = tte.created_by_user_id
         WHERE tte.transfer_id = :transfer_id
         ORDER BY tte.created_at DESC, tte.id DESC',
        ['transfer_id' => $transferId]
    );
}

function transfer_offer_list(int $transferId): array
{
    return db_fetch_all(
        'SELECT tf.*, offer_user.full_name AS offered_by_name,
                offer_club.name AS offered_by_club_name
         FROM transfer_offers tf
         INNER JOIN users offer_user ON offer_user.id = tf.offered_by_user_id
         INNER JOIN clubs offer_club ON offer_club.id = tf.offered_by_club_id
         WHERE tf.transfer_id = :transfer_id
         ORDER BY tf.created_at DESC, tf.id DESC',
        ['transfer_id' => $transferId]
    );
}

function transfer_latest_open_offer(int $transferId): ?array
{
    return db_fetch_one(
        'SELECT tf.*, offer_user.full_name AS offered_by_name,
                offer_club.name AS offered_by_club_name
         FROM transfer_offers tf
         INNER JOIN users offer_user ON offer_user.id = tf.offered_by_user_id
         INNER JOIN clubs offer_club ON offer_club.id = tf.offered_by_club_id
         WHERE tf.transfer_id = :transfer_id
           AND tf.status = "proposed"
         ORDER BY tf.created_at DESC, tf.id DESC
         LIMIT 1',
        ['transfer_id' => $transferId]
    );
}

function transfer_approval_map(int $transferId): array
{
    $approvalRows = db_fetch_all(
        'SELECT *
         FROM transfer_approvals
         WHERE transfer_id = :transfer_id',
        ['transfer_id' => $transferId]
    );

    $approvalMap = [];

    foreach ($approvalRows as $approvalRow) {
        $approvalMap[$approvalRow['approval_type']] = $approvalRow;
    }

    return $approvalMap;
}

function transfer_can_review_interest(array $transfer): bool
{
    return can_manage_transfers()
        && is_club_admin()
        && user_club_id() === (int) $transfer['selling_club_id']
        && (string) $transfer['current_stage'] === 'interest_registered';
}

function transfer_can_submit_offer(array $transfer): bool
{
    return can_manage_transfers()
        && is_club_admin()
        && user_club_id() === (int) $transfer['buying_club_id']
        && (string) $transfer['current_stage'] === 'negotiation';
}

function transfer_can_review_offer(array $transfer, ?array $offer = null): bool
{
    if (!can_manage_transfers() || !is_club_admin() || user_club_id() !== (int) $transfer['selling_club_id']) {
        return false;
    }

    if ((string) $transfer['current_stage'] !== 'negotiation') {
        return false;
    }

    return $offer !== null && (string) ($offer['status'] ?? '') === 'proposed';
}

function transfer_can_confirm_buyer_approval(array $transfer, array $approvalMap): bool
{
    return can_manage_transfers()
        && is_club_admin()
        && user_club_id() === (int) $transfer['buying_club_id']
        && (string) $transfer['current_stage'] === 'verbal_agreement'
        && ($approvalMap['buying_club']['status'] ?? 'pending') === 'pending';
}

function transfer_can_confirm_league_approval(array $transfer, array $approvalMap): bool
{
    return can_approve_transfers()
        && !is_club_admin()
        && (string) $transfer['current_stage'] === 'awaiting_league_approval'
        && ($approvalMap['league']['status'] ?? 'pending') === 'pending';
}

function transfer_can_finalize_buying_club_approval(array $transfer, array $approvalMap): bool
{
    return can_manage_transfers()
        && is_club_admin()
        && user_club_id() === (int) $transfer['buying_club_id']
        && (string) $transfer['current_stage'] === 'verbal_agreement'
        && ($approvalMap['buying_club']['status'] ?? 'pending') === 'pending';
}

function transfer_can_manage_medical(array $transfer): bool
{
    return can_manage_transfers()
        && is_club_admin()
        && user_club_id() === (int) $transfer['buying_club_id']
        && in_array((string) $transfer['current_stage'], ['approved', 'medical'], true)
        && !in_array((string) $transfer['medical_status'], ['completed', 'failed'], true);
}

function transfer_offer_badge_class(string $status): string
{
    return match ($status) {
        'accepted' => 'badge-soft-success',
        'rejected', 'withdrawn' => 'badge-soft-danger',
        default => 'text-bg-light',
    };
}

function transfer_offer_status_label(string $status): string
{
    return ucwords(str_replace('_', ' ', $status));
}

function transfer_active_interest_exists(int $playerId, int $buyingClubId): bool
{
    return (int) db_fetch_value(
        'SELECT COUNT(*)
         FROM transfers
         WHERE player_id = :player_id
           AND buying_club_id = :buying_club_id
           AND current_stage NOT IN ("completed", "rejected", "cancelled")',
        ['player_id' => $playerId, 'buying_club_id' => $buyingClubId],
        0
    ) > 0;
}

function validate_transfer_review_payload(array $data, array &$errors): array
{
    $transferId = (int) ($data['transfer_id'] ?? 0);
    $transfer = $transferId > 0 ? get_transfer_by_id($transferId) : null;

    if ($transfer === null) {
        $errors['transfer_id'] = 'Selected transfer record is invalid.';
    } elseif (!transfer_can_review_interest($transfer)) {
        $errors['transfer_id'] = 'This transfer interest is not available for seller review.';
    }

    $decision = trim((string) ($data['decision'] ?? ''));
    if (!in_array($decision, ['accept', 'reject'], true)) {
        $errors['decision'] = 'Transfer decision selection is invalid.';
    }

    $notes = trim((string) ($data['decision_notes'] ?? ''));
    if ($decision === 'reject' && $notes === '') {
        $errors['decision_notes'] = 'Please provide a reason when rejecting transfer interest.';
    }

    return [
        'transfer_id' => $transferId,
        'transfer' => $transfer,
        'decision' => $decision,
        'decision_notes' => $notes,
    ];
}

function validate_transfer_offer_payload(array $data, array &$errors): array
{
    $transferId = (int) ($data['transfer_id'] ?? 0);
    $transfer = $transferId > 0 ? get_transfer_by_id($transferId) : null;

    if ($transfer === null) {
        $errors['transfer_id'] = 'Selected transfer record is invalid.';
    } elseif (!transfer_can_submit_offer($transfer)) {
        $errors['transfer_id'] = 'This transfer is not available for offer negotiation from your club.';
    }

    if ($transfer !== null && transfer_latest_open_offer((int) $transfer['id']) !== null) {
        $errors['transfer_id'] = 'There is already a pending offer awaiting seller response.';
    }

    validate_numeric_min($data, 'offered_fee', 'Offered fee', 0, $errors);
    $offeredFee = trim((string) ($data['offered_fee'] ?? ''));
    if ($offeredFee === '') {
        $errors['offered_fee'] = 'Offered fee is required.';
    }

    validate_numeric_min($data, 'contract_years', 'Contract years', 1, $errors);
    $contractYears = trim((string) ($data['contract_years'] ?? ''));
    if ($contractYears === '') {
        $errors['contract_years'] = 'Contract years is required.';
    }

    validate_date_field($data, 'release_date', 'Proposed release date', $errors);
    $notes = trim((string) ($data['offer_notes'] ?? ''));

    return [
        'transfer_id' => $transferId,
        'transfer' => $transfer,
        'offered_fee' => $offeredFee === '' ? null : (float) $offeredFee,
        'contract_years' => $contractYears === '' ? null : (int) $contractYears,
        'release_date' => trim((string) ($data['release_date'] ?? '')) ?: null,
        'offer_notes' => $notes,
    ];
}

function validate_transfer_offer_review_payload(array $data, array &$errors): array
{
    $transferId = (int) ($data['transfer_id'] ?? 0);
    $offerId = (int) ($data['offer_id'] ?? 0);
    $transfer = $transferId > 0 ? get_transfer_by_id($transferId) : null;
    $offer = $offerId > 0 ? db_fetch_one('SELECT * FROM transfer_offers WHERE id = :id AND transfer_id = :transfer_id LIMIT 1', ['id' => $offerId, 'transfer_id' => $transferId]) : null;

    if ($transfer === null) {
        $errors['transfer_id'] = 'Selected transfer record is invalid.';
    }

    if ($offer === null) {
        $errors['offer_id'] = 'Selected transfer offer is invalid.';
    }

    if ($transfer !== null && !transfer_can_review_offer($transfer, $offer)) {
        $errors['offer_id'] = 'This offer is not available for seller review.';
    }

    $decision = trim((string) ($data['decision'] ?? ''));
    if (!in_array($decision, ['accept', 'reject'], true)) {
        $errors['decision'] = 'Offer decision selection is invalid.';
    }

    $notes = trim((string) ($data['decision_notes'] ?? ''));
    if ($decision === 'reject' && $notes === '') {
        $errors['decision_notes'] = 'Please provide a reason when rejecting the offer.';
    }

    return [
        'transfer_id' => $transferId,
        'transfer' => $transfer,
        'offer_id' => $offerId,
        'offer' => $offer,
        'decision' => $decision,
        'decision_notes' => $notes,
    ];
}

function validate_transfer_approval_payload(array $data, array &$errors): array
{
    $transferId = (int) ($data['transfer_id'] ?? 0);
    $transfer = $transferId > 0 ? get_transfer_by_id($transferId) : null;
    $approvalMap = $transferId > 0 ? transfer_approval_map($transferId) : [];

    if ($transfer === null) {
        $errors['transfer_id'] = 'Selected transfer record is invalid.';
    }

    $approvalType = trim((string) ($data['approval_type'] ?? ''));
    if (!in_array($approvalType, ['buying_club', 'league'], true)) {
        $errors['approval_type'] = 'Approval checkpoint selection is invalid.';
    }

    $decision = trim((string) ($data['decision'] ?? ''));
    if (!in_array($decision, ['accept', 'reject'], true)) {
        $errors['decision'] = 'Approval decision selection is invalid.';
    }

    $notes = trim((string) ($data['decision_notes'] ?? ''));
    if ($decision === 'reject' && $notes === '') {
        $errors['decision_notes'] = 'Please provide a reason when rejecting approval.';
    }

    if ($transfer !== null) {
        $canApprove = $approvalType === 'buying_club'
            ? transfer_can_finalize_buying_club_approval($transfer, $approvalMap)
            : transfer_can_confirm_league_approval($transfer, $approvalMap);

        if (!$canApprove) {
            $errors['approval_type'] = 'This approval step is not available for your account right now.';
        }
    }

    return [
        'transfer_id' => $transferId,
        'transfer' => $transfer,
        'approval_map' => $approvalMap,
        'approval_type' => $approvalType,
        'decision' => $decision,
        'decision_notes' => $notes,
    ];
}

function validate_transfer_medical_payload(array $data, array &$errors): array
{
    $transferId = (int) ($data['transfer_id'] ?? 0);
    $transfer = $transferId > 0 ? get_transfer_by_id($transferId) : null;

    if ($transfer === null) {
        $errors['transfer_id'] = 'Selected transfer record is invalid.';
    } elseif (!transfer_can_manage_medical($transfer)) {
        $errors['transfer_id'] = 'This transfer is not available for medical completion from your club.';
    }

    $decision = trim((string) ($data['decision'] ?? ''));
    if (!in_array($decision, ['complete', 'fail'], true)) {
        $errors['decision'] = 'Medical decision selection is invalid.';
    }

    validate_date_field($data, 'effective_date', 'Effective date', $errors);
    $notes = trim((string) ($data['medical_notes'] ?? ''));
    if ($decision === 'fail' && $notes === '') {
        $errors['medical_notes'] = 'Please provide a reason when medical is failed.';
    }

    return [
        'transfer_id' => $transferId,
        'transfer' => $transfer,
        'decision' => $decision,
        'effective_date' => trim((string) ($data['effective_date'] ?? '')) ?: null,
        'medical_notes' => $notes,
    ];
}

function validate_transfer_interest_payload(array $data, array &$errors): array
{
    $playerId = (int) ($data['player_id'] ?? 0);
    $player = $playerId > 0 ? get_transfer_market_player_by_id($playerId) : null;

    if ($player === null) {
        $errors['player_id'] = 'Selected player is not available for transfer interest.';
    }

    $buyingClubId = is_club_admin() ? (int) user_club_id() : (int) ($data['buying_club_id'] ?? 0);
    if ($buyingClubId <= 0 || get_club_by_id($buyingClubId) === null) {
        $errors['buying_club_id'] = 'Buying club is invalid.';
    }

    $window = transfer_open_window();
    if ($window === null) {
        $errors['window_id'] = 'No open transfer window is available.';
    }

    $message = trim((string) ($data['interest_message'] ?? ''));

    if ($player !== null && $buyingClubId > 0) {
        if ((int) $player['club_id'] === $buyingClubId) {
            $errors['buying_club_id'] = 'You cannot register interest in a player from your own club.';
        }

        if (transfer_active_interest_exists((int) $player['id'], $buyingClubId)) {
            $errors['player_id'] = 'An active transfer interest for this player already exists from the selected club.';
        }
    }

    return [
        'player_id' => $playerId,
        'player' => $player,
        'selling_club_id' => $player !== null ? (int) $player['club_id'] : 0,
        'buying_club_id' => $buyingClubId,
        'window_id' => (int) ($window['id'] ?? 0) ?: null,
        'interest_message' => $message,
    ];
}

function transfer_news_slugify(string $value): string
{
    $value = strtolower(trim($value));
    $value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? '';
    $value = trim($value, '-');

    return $value !== '' ? $value : 'transfer-update';
}

function transfer_unique_news_slug(string $headline): string
{
    $baseSlug = transfer_news_slugify($headline);
    $slug = $baseSlug;
    $counter = 2;

    while ((int) db_fetch_value('SELECT COUNT(*) FROM news_posts WHERE slug = :slug', ['slug' => $slug], 0) > 0) {
        $slug = $baseSlug . '-' . $counter;
        $counter++;
    }

    return $slug;
}

function transfer_publish_news(string $headline, string $body): int
{
    $payload = [
        'title' => $headline,
        'slug' => transfer_unique_news_slug($headline),
        'body' => $body,
        'featured_image' => null,
        'status' => 'published',
        'created_by' => (int) (current_user()['id'] ?? 0) ?: null,
        'published_at' => date('Y-m-d H:i:s'),
    ];

    db_query(
        'INSERT INTO news_posts (
            title, slug, body, featured_image, status, created_by, published_at, created_at
         ) VALUES (
            :title, :slug, :body, :featured_image, :status, :created_by, :published_at, NOW()
         )',
        $payload
    );

    $postId = (int) db()->lastInsertId();
    web_push_send_news_post($postId);

    return $postId;
}

function transfer_create_notifications_for_club(int $transferId, int $clubId, string $title, string $body, string $notificationType = 'interest_registered'): void
{
    $recipients = db_fetch_all(
        'SELECT DISTINCT u.id
         FROM users u
         INNER JOIN role_permissions rp ON rp.role_id = u.role_id
         INNER JOIN permissions p ON p.id = rp.permission_id
         WHERE u.club_id = :club_id
           AND u.status = "active"
           AND p.permission_key = "manage_transfers"',
        ['club_id' => $clubId]
    );

    foreach ($recipients as $recipient) {
        db_query(
            'INSERT INTO transfer_notifications (transfer_id, recipient_user_id, notification_type, title, body, is_read, created_at)
             VALUES (:transfer_id, :recipient_user_id, :notification_type, :title, :body, 0, NOW())',
            [
                'transfer_id' => $transferId,
                'recipient_user_id' => (int) $recipient['id'],
                'notification_type' => $notificationType,
                'title' => $title,
                'body' => $body,
            ]
        );
    }
}

function transfer_create_notifications_for_permission(int $transferId, string $permissionKey, string $title, string $body, string $notificationType): void
{
    $recipients = db_fetch_all(
        'SELECT DISTINCT u.id
         FROM users u
         INNER JOIN role_permissions rp ON rp.role_id = u.role_id
         INNER JOIN permissions p ON p.id = rp.permission_id
         WHERE u.status = "active"
           AND p.permission_key = :permission_key',
        ['permission_key' => $permissionKey]
    );

    foreach ($recipients as $recipient) {
        db_query(
            'INSERT INTO transfer_notifications (transfer_id, recipient_user_id, notification_type, title, body, is_read, created_at)
             VALUES (:transfer_id, :recipient_user_id, :notification_type, :title, :body, 0, NOW())',
            [
                'transfer_id' => $transferId,
                'recipient_user_id' => (int) $recipient['id'],
                'notification_type' => $notificationType,
                'title' => $title,
                'body' => $body,
            ]
        );
    }
}

function transfer_record_selling_club_decision(array $payload): void
{
    $transfer = $payload['transfer'];
    $actor = current_user();
    $playerName = trim(($transfer['first_name'] ?? '') . ' ' . ($transfer['last_name'] ?? ''));

    if ($payload['decision'] === 'accept') {
        $headline = sprintf(
            '%s have accepted %s\'s interest in %s',
            $transfer['selling_club_name'] ?? 'The selling club',
            $transfer['buying_club_name'] ?? 'the buying club',
            $playerName
        );
        $body = sprintf(
            '%s have accepted the transfer interest submitted by %s for %s. The move advances to negotiation.%s',
            $transfer['selling_club_name'] ?? 'The selling club',
            $transfer['buying_club_name'] ?? 'the buying club',
            $playerName,
            $payload['decision_notes'] !== '' ? ' Seller note: ' . $payload['decision_notes'] : ''
        );
        $nextStage = 'negotiation';
        $approvalStatus = 'accepted';
        $notificationType = 'interest_accepted';
        $flashVerb = 'accepted';
    } else {
        $headline = sprintf(
            '%s have rejected %s\'s interest in %s',
            $transfer['selling_club_name'] ?? 'The selling club',
            $transfer['buying_club_name'] ?? 'the buying club',
            $playerName
        );
        $body = sprintf(
            '%s have rejected the transfer interest submitted by %s for %s.%s',
            $transfer['selling_club_name'] ?? 'The selling club',
            $transfer['buying_club_name'] ?? 'the buying club',
            $playerName,
            $payload['decision_notes'] !== '' ? ' Seller reason: ' . $payload['decision_notes'] : ''
        );
        $nextStage = 'rejected';
        $approvalStatus = 'rejected';
        $notificationType = 'interest_rejected';
        $flashVerb = 'rejected';
    }

    db()->beginTransaction();

    try {
        db_query(
            'UPDATE transfer_approvals
             SET status = :status,
                 acted_by_user_id = :acted_by_user_id,
                 acted_at = NOW(),
                 notes = :notes
             WHERE transfer_id = :transfer_id
               AND approval_type = "selling_club"',
            [
                'status' => $approvalStatus,
                'acted_by_user_id' => (int) ($actor['id'] ?? 0) ?: null,
                'notes' => $payload['decision_notes'] !== '' ? $payload['decision_notes'] : null,
                'transfer_id' => (int) $transfer['id'],
            ]
        );

        db_query(
            'UPDATE transfers
             SET current_stage = :current_stage,
                 rejection_reason = :rejection_reason,
                 updated_at = NOW()
             WHERE id = :id',
            [
                'current_stage' => $nextStage,
                'rejection_reason' => $payload['decision'] === 'reject' && $payload['decision_notes'] !== '' ? $payload['decision_notes'] : null,
                'id' => (int) $transfer['id'],
            ]
        );

        $newsPostId = transfer_publish_news($headline, $body);

        db_query(
            'INSERT INTO transfer_timeline_events (transfer_id, event_type, headline, body, publish_news, news_post_id, created_by_user_id, created_at)
             VALUES (:transfer_id, :event_type, :headline, :body, :publish_news, :news_post_id, :created_by_user_id, NOW())',
            [
                'transfer_id' => (int) $transfer['id'],
                'event_type' => $notificationType,
                'headline' => $headline,
                'body' => $body,
                'publish_news' => 1,
                'news_post_id' => $newsPostId,
                'created_by_user_id' => (int) ($actor['id'] ?? 0) ?: null,
            ]
        );

        transfer_create_notifications_for_club(
            (int) $transfer['id'],
            (int) $transfer['buying_club_id'],
            'Transfer interest ' . $flashVerb,
            $headline,
            $notificationType
        );

        transfer_mark_notifications_read((int) $transfer['id'], (int) ($actor['id'] ?? 0));

        audit_log('review_transfer_interest', 'transfers', (int) $transfer['id'], null, [
            'decision' => $payload['decision'],
            'decision_notes' => $payload['decision_notes'],
            'next_stage' => $nextStage,
            'news_post_headline' => $headline,
        ]);

        db()->commit();
    } catch (Throwable $exception) {
        if (db()->inTransaction()) {
            db()->rollBack();
        }

        throw $exception;
    }
}

function transfer_submit_offer(array $payload): int
{
    $transfer = $payload['transfer'];
    $actor = current_user();
    $playerName = trim(($transfer['first_name'] ?? '') . ' ' . ($transfer['last_name'] ?? ''));
    $headline = sprintf(
        '%s table an offer for %s',
        $transfer['buying_club_name'] ?? 'The buying club',
        $playerName
    );
    $body = sprintf(
        '%s have submitted an offer of %s for %s on a %s-year contract.%s%s',
        $transfer['buying_club_name'] ?? 'The buying club',
        number_format((float) $payload['offered_fee'], 2),
        $playerName,
        (string) $payload['contract_years'],
        $payload['release_date'] !== null ? ' Proposed release date: ' . $payload['release_date'] . '.' : '',
        $payload['offer_notes'] !== '' ? ' Buyer note: ' . $payload['offer_notes'] : ''
    );

    db()->beginTransaction();

    try {
        db_query(
            'INSERT INTO transfer_offers (
                transfer_id, offered_by_club_id, offered_by_user_id, offered_fee, contract_years, release_date, status, notes, created_at
             ) VALUES (
                :transfer_id, :offered_by_club_id, :offered_by_user_id, :offered_fee, :contract_years, :release_date, :status, :notes, NOW()
             )',
            [
                'transfer_id' => (int) $transfer['id'],
                'offered_by_club_id' => (int) $transfer['buying_club_id'],
                'offered_by_user_id' => (int) ($actor['id'] ?? 0),
                'offered_fee' => $payload['offered_fee'],
                'contract_years' => $payload['contract_years'],
                'release_date' => $payload['release_date'],
                'status' => 'proposed',
                'notes' => $payload['offer_notes'] !== '' ? $payload['offer_notes'] : null,
            ]
        );

        $offerId = (int) db()->lastInsertId();

        db_query(
            'UPDATE transfers
             SET current_stage = "negotiation",
                 proposed_fee = :proposed_fee,
                 contract_years = :contract_years,
                 proposed_release_date = :proposed_release_date,
                 updated_at = NOW()
             WHERE id = :id',
            [
                'proposed_fee' => $payload['offered_fee'],
                'contract_years' => $payload['contract_years'],
                'proposed_release_date' => $payload['release_date'],
                'id' => (int) $transfer['id'],
            ]
        );

        $newsPostId = transfer_publish_news($headline, $body);

        db_query(
            'INSERT INTO transfer_timeline_events (transfer_id, event_type, headline, body, publish_news, news_post_id, created_by_user_id, created_at)
             VALUES (:transfer_id, :event_type, :headline, :body, :publish_news, :news_post_id, :created_by_user_id, NOW())',
            [
                'transfer_id' => (int) $transfer['id'],
                'event_type' => 'offer_submitted',
                'headline' => $headline,
                'body' => $body,
                'publish_news' => 1,
                'news_post_id' => $newsPostId,
                'created_by_user_id' => (int) ($actor['id'] ?? 0) ?: null,
            ]
        );

        transfer_create_notifications_for_club((int) $transfer['id'], (int) $transfer['selling_club_id'], 'New transfer offer submitted', $headline, 'offer_submitted');

        audit_log('submit_transfer_offer', 'transfer_offers', $offerId, null, [
            'transfer_id' => (int) $transfer['id'],
            'offered_fee' => $payload['offered_fee'],
            'contract_years' => $payload['contract_years'],
            'release_date' => $payload['release_date'],
            'offer_notes' => $payload['offer_notes'],
        ]);

        db()->commit();

        return $offerId;
    } catch (Throwable $exception) {
        if (db()->inTransaction()) {
            db()->rollBack();
        }

        throw $exception;
    }
}

function transfer_record_offer_decision(array $payload): void
{
    $transfer = $payload['transfer'];
    $offer = $payload['offer'];
    $actor = current_user();
    $playerName = trim(($transfer['first_name'] ?? '') . ' ' . ($transfer['last_name'] ?? ''));

    if ($payload['decision'] === 'accept') {
        $offerStatus = 'accepted';
        $nextStage = 'verbal_agreement';
        $headline = sprintf('%s accept the offer for %s', $transfer['selling_club_name'] ?? 'The selling club', $playerName);
        $body = sprintf(
            '%s have accepted %s\'s offer of %s for %s.%s',
            $transfer['selling_club_name'] ?? 'The selling club',
            $transfer['buying_club_name'] ?? 'the buying club',
            number_format((float) $offer['offered_fee'], 2),
            $playerName,
            $payload['decision_notes'] !== '' ? ' Seller note: ' . $payload['decision_notes'] : ''
        );
        $notificationType = 'offer_accepted';
        $notificationTitle = 'Transfer offer accepted';
    } else {
        $offerStatus = 'rejected';
        $nextStage = 'negotiation';
        $headline = sprintf('%s reject the offer for %s', $transfer['selling_club_name'] ?? 'The selling club', $playerName);
        $body = sprintf(
            '%s have rejected %s\'s offer of %s for %s.%s',
            $transfer['selling_club_name'] ?? 'The selling club',
            $transfer['buying_club_name'] ?? 'the buying club',
            number_format((float) $offer['offered_fee'], 2),
            $playerName,
            $payload['decision_notes'] !== '' ? ' Seller reason: ' . $payload['decision_notes'] : ''
        );
        $notificationType = 'offer_rejected';
        $notificationTitle = 'Transfer offer rejected';
    }

    db()->beginTransaction();

    try {
        db_query(
            'UPDATE transfer_offers
             SET status = :status,
                 notes = :notes
             WHERE id = :id',
            [
                'status' => $offerStatus,
                'notes' => $payload['decision_notes'] !== '' ? $payload['decision_notes'] : ($offer['notes'] ?? null),
                'id' => (int) $offer['id'],
            ]
        );

        db_query(
            'UPDATE transfers
             SET current_stage = :current_stage,
                 agreed_fee = :agreed_fee,
                 proposed_fee = :proposed_fee,
                 contract_years = :contract_years,
                 proposed_release_date = :proposed_release_date,
                 updated_at = NOW()
             WHERE id = :id',
            [
                'current_stage' => $nextStage,
                'agreed_fee' => $payload['decision'] === 'accept' ? $offer['offered_fee'] : null,
                'proposed_fee' => $payload['decision'] === 'accept' ? $offer['offered_fee'] : $transfer['proposed_fee'],
                'contract_years' => $offer['contract_years'],
                'proposed_release_date' => $offer['release_date'],
                'id' => (int) $transfer['id'],
            ]
        );

        $newsPostId = transfer_publish_news($headline, $body);

        db_query(
            'INSERT INTO transfer_timeline_events (transfer_id, event_type, headline, body, publish_news, news_post_id, created_by_user_id, created_at)
             VALUES (:transfer_id, :event_type, :headline, :body, :publish_news, :news_post_id, :created_by_user_id, NOW())',
            [
                'transfer_id' => (int) $transfer['id'],
                'event_type' => $notificationType,
                'headline' => $headline,
                'body' => $body,
                'publish_news' => 1,
                'news_post_id' => $newsPostId,
                'created_by_user_id' => (int) ($actor['id'] ?? 0) ?: null,
            ]
        );

        transfer_create_notifications_for_club((int) $transfer['id'], (int) $transfer['buying_club_id'], $notificationTitle, $headline, $notificationType);
        transfer_mark_notifications_read((int) $transfer['id'], (int) ($actor['id'] ?? 0));

        audit_log('review_transfer_offer', 'transfer_offers', (int) $offer['id'], null, [
            'transfer_id' => (int) $transfer['id'],
            'decision' => $payload['decision'],
            'decision_notes' => $payload['decision_notes'],
            'next_stage' => $nextStage,
        ]);

        db()->commit();
    } catch (Throwable $exception) {
        if (db()->inTransaction()) {
            db()->rollBack();
        }

        throw $exception;
    }
}

function transfer_record_approval_decision(array $payload): void
{
    $transfer = $payload['transfer'];
    $actor = current_user();
    $playerName = trim(($transfer['first_name'] ?? '') . ' ' . ($transfer['last_name'] ?? ''));
    $approvalType = $payload['approval_type'];
    $decision = $payload['decision'];
    $notes = $payload['decision_notes'];

    if ($approvalType === 'buying_club') {
        if ($decision === 'accept') {
            $nextStage = 'awaiting_league_approval';
            $approvalStatus = 'accepted';
            $headline = sprintf('%s confirm the verbal agreement for %s', $transfer['buying_club_name'] ?? 'The buying club', $playerName);
            $body = sprintf(
                '%s have confirmed the agreed terms for %s. The transfer now awaits league approval.%s',
                $transfer['buying_club_name'] ?? 'The buying club',
                $playerName,
                $notes !== '' ? ' Buyer note: ' . $notes : ''
            );
            $notificationType = 'buying_club_approved';
            $notificationTitle = 'Buying club approved transfer';
        } else {
            $nextStage = 'negotiation';
            $approvalStatus = 'rejected';
            $headline = sprintf('%s reject the verbal agreement for %s', $transfer['buying_club_name'] ?? 'The buying club', $playerName);
            $body = sprintf(
                '%s have rejected the agreed terms for %s. The transfer returns to negotiation.%s',
                $transfer['buying_club_name'] ?? 'The buying club',
                $playerName,
                $notes !== '' ? ' Buyer reason: ' . $notes : ''
            );
            $notificationType = 'buying_club_rejected';
            $notificationTitle = 'Buying club rejected agreement';
        }
    } else {
        if ($decision === 'accept') {
            $nextStage = 'approved';
            $approvalStatus = 'accepted';
            $headline = sprintf('League approval granted for %s', $playerName);
            $body = sprintf(
                'League management have approved the transfer of %s between %s and %s.%s',
                $playerName,
                $transfer['selling_club_name'] ?? 'the selling club',
                $transfer['buying_club_name'] ?? 'the buying club',
                $notes !== '' ? ' League note: ' . $notes : ''
            );
            $notificationType = 'league_approved';
            $notificationTitle = 'League approval granted';
        } else {
            $nextStage = 'negotiation';
            $approvalStatus = 'rejected';
            $headline = sprintf('League approval denied for %s', $playerName);
            $body = sprintf(
                'League management have denied approval for the transfer of %s between %s and %s.%s',
                $playerName,
                $transfer['selling_club_name'] ?? 'the selling club',
                $transfer['buying_club_name'] ?? 'the buying club',
                $notes !== '' ? ' League reason: ' . $notes : ''
            );
            $notificationType = 'league_rejected';
            $notificationTitle = 'League approval denied';
        }
    }

    db()->beginTransaction();

    try {
        db_query(
            'UPDATE transfer_approvals
             SET status = :status,
                 acted_by_user_id = :acted_by_user_id,
                 acted_at = NOW(),
                 notes = :notes
             WHERE transfer_id = :transfer_id
               AND approval_type = :approval_type',
            [
                'status' => $approvalStatus,
                'acted_by_user_id' => (int) ($actor['id'] ?? 0) ?: null,
                'notes' => $notes !== '' ? $notes : null,
                'transfer_id' => (int) $transfer['id'],
                'approval_type' => $approvalType,
            ]
        );

        if ($approvalType === 'buying_club' && $decision === 'reject') {
            db_query(
                'UPDATE transfer_approvals
                 SET status = "pending",
                     acted_by_user_id = NULL,
                     acted_at = NULL,
                     notes = NULL
                 WHERE transfer_id = :transfer_id
                   AND approval_type = "buying_club"',
                ['transfer_id' => (int) $transfer['id']]
            );
        }

        if ($approvalType === 'league' && $decision === 'reject') {
            db_query(
                'UPDATE transfer_approvals
                 SET status = "pending",
                     acted_by_user_id = NULL,
                     acted_at = NULL,
                     notes = NULL
                 WHERE transfer_id = :transfer_id
                   AND approval_type IN ("buying_club", "league")',
                ['transfer_id' => (int) $transfer['id']]
            );
        }

        db_query(
            'UPDATE transfers
             SET current_stage = :current_stage,
                 updated_at = NOW()
             WHERE id = :id',
            [
                'current_stage' => $nextStage,
                'id' => (int) $transfer['id'],
            ]
        );

        $newsPostId = transfer_publish_news($headline, $body);

        db_query(
            'INSERT INTO transfer_timeline_events (transfer_id, event_type, headline, body, publish_news, news_post_id, created_by_user_id, created_at)
             VALUES (:transfer_id, :event_type, :headline, :body, :publish_news, :news_post_id, :created_by_user_id, NOW())',
            [
                'transfer_id' => (int) $transfer['id'],
                'event_type' => $notificationType,
                'headline' => $headline,
                'body' => $body,
                'publish_news' => 1,
                'news_post_id' => $newsPostId,
                'created_by_user_id' => (int) ($actor['id'] ?? 0) ?: null,
            ]
        );

        if ($approvalType === 'buying_club' && $decision === 'accept') {
            transfer_create_notifications_for_permission((int) $transfer['id'], 'approve_transfers', 'Transfer awaiting league approval', $headline, $notificationType);
            transfer_create_notifications_for_club((int) $transfer['id'], (int) $transfer['selling_club_id'], $notificationTitle, $headline, $notificationType);
        } else {
            transfer_create_notifications_for_club((int) $transfer['id'], (int) $transfer['selling_club_id'], $notificationTitle, $headline, $notificationType);
            transfer_create_notifications_for_club((int) $transfer['id'], (int) $transfer['buying_club_id'], $notificationTitle, $headline, $notificationType);
        }

        transfer_mark_notifications_read((int) $transfer['id'], (int) ($actor['id'] ?? 0));

        audit_log('review_transfer_approval', 'transfer_approvals', (int) $transfer['id'], null, [
            'approval_type' => $approvalType,
            'decision' => $decision,
            'decision_notes' => $notes,
            'next_stage' => $nextStage,
        ]);

        db()->commit();
    } catch (Throwable $exception) {
        if (db()->inTransaction()) {
            db()->rollBack();
        }

        throw $exception;
    }
}

function transfer_record_medical_decision(array $payload): void
{
    $transfer = $payload['transfer'];
    $actor = current_user();
    $playerName = trim(($transfer['first_name'] ?? '') . ' ' . ($transfer['last_name'] ?? ''));
    $effectiveDate = $payload['effective_date'] ?? date('Y-m-d');
    $destinationClubId = $payload['decision'] === 'complete' ? (int) $transfer['buying_club_id'] : (int) $transfer['selling_club_id'];
    $registrationNumber = player_generate_registration_number(
        $destinationClubId,
        (int) $transfer['player_id'],
        (string) ($transfer['registration_number'] ?? '')
    );

    if ($payload['decision'] === 'complete') {
        $headline = sprintf('Medical complete: %s joins %s', $playerName, $transfer['buying_club_name'] ?? 'the buying club');
        $body = sprintf(
            '%s has completed medical formalities and officially joins %s from %s.%s',
            $playerName,
            $transfer['buying_club_name'] ?? 'the buying club',
            $transfer['selling_club_name'] ?? 'the selling club',
            $payload['medical_notes'] !== '' ? ' Club note: ' . $payload['medical_notes'] : ''
        );
        $nextStage = 'completed';
        $medicalStatus = 'completed';
        $playerMedicalStatus = 'fit';
        $playerTransferStatus = 'transferred';
        $notificationType = 'medical_completed';
        $notificationTitle = 'Medical completed';
        $completedAt = date('Y-m-d H:i:s');
        $rejectionReason = null;
    } else {
        $headline = sprintf('Medical failed: %s transfer to %s is off', $playerName, $transfer['buying_club_name'] ?? 'the buying club');
        $body = sprintf(
            '%s did not complete medical formalities for the move to %s, so the transfer has been cancelled.%s',
            $playerName,
            $transfer['buying_club_name'] ?? 'the buying club',
            $payload['medical_notes'] !== '' ? ' Medical note: ' . $payload['medical_notes'] : ''
        );
        $nextStage = 'cancelled';
        $medicalStatus = 'failed';
        $playerMedicalStatus = 'unfit';
        $playerTransferStatus = 'active';
        $notificationType = 'medical_failed';
        $notificationTitle = 'Medical failed';
        $completedAt = null;
        $rejectionReason = $payload['medical_notes'] !== '' ? $payload['medical_notes'] : 'Medical failed.';
    }

    db()->beginTransaction();

    try {
        db_query(
            'UPDATE transfers
             SET current_stage = :current_stage,
                 medical_status = :medical_status,
                 effective_date = :effective_date,
                 completed_at = :completed_at,
                 rejection_reason = :rejection_reason,
                 updated_at = NOW()
             WHERE id = :id',
            [
                'current_stage' => $nextStage,
                'medical_status' => $medicalStatus,
                'effective_date' => $effectiveDate,
                'completed_at' => $completedAt,
                'rejection_reason' => $rejectionReason,
                'id' => (int) $transfer['id'],
            ]
        );

        db_query(
            'UPDATE players
             SET medical_status = :medical_status,
                 transfer_status = :transfer_status,
                 club_id = :club_id,
                 registration_number = :registration_number,
                 updated_at = NOW()
             WHERE id = :id',
            [
                'medical_status' => $playerMedicalStatus,
                'transfer_status' => $playerTransferStatus,
                'club_id' => $destinationClubId,
                'registration_number' => $registrationNumber,
                'id' => (int) $transfer['player_id'],
            ]
        );

        $newsPostId = transfer_publish_news($headline, $body);

        db_query(
            'INSERT INTO transfer_timeline_events (transfer_id, event_type, headline, body, publish_news, news_post_id, created_by_user_id, created_at)
             VALUES (:transfer_id, :event_type, :headline, :body, :publish_news, :news_post_id, :created_by_user_id, NOW())',
            [
                'transfer_id' => (int) $transfer['id'],
                'event_type' => $notificationType,
                'headline' => $headline,
                'body' => $body,
                'publish_news' => 1,
                'news_post_id' => $newsPostId,
                'created_by_user_id' => (int) ($actor['id'] ?? 0) ?: null,
            ]
        );

        transfer_create_notifications_for_club((int) $transfer['id'], (int) $transfer['selling_club_id'], $notificationTitle, $headline, $notificationType);
        transfer_create_notifications_for_club((int) $transfer['id'], (int) $transfer['buying_club_id'], $notificationTitle, $headline, $notificationType);
        transfer_mark_notifications_read((int) $transfer['id'], (int) ($actor['id'] ?? 0));

        audit_log('record_transfer_medical', 'transfers', (int) $transfer['id'], null, [
            'decision' => $payload['decision'],
            'medical_notes' => $payload['medical_notes'],
            'effective_date' => $effectiveDate,
            'next_stage' => $nextStage,
            'player_destination_club_id' => $payload['decision'] === 'complete' ? (int) $transfer['buying_club_id'] : (int) $transfer['selling_club_id'],
        ]);

        db()->commit();
    } catch (Throwable $exception) {
        if (db()->inTransaction()) {
            db()->rollBack();
        }

        throw $exception;
    }
}

function transfer_register_interest(array $payload): int
{
    $player = $payload['player'];
    $buyingClub = get_club_by_id((int) $payload['buying_club_id']);
    $sellingClub = get_club_by_id((int) $payload['selling_club_id']);
    $actor = current_user();

    $headline = sprintf(
        '%s is interested in %s from %s',
        $actor['full_name'] ?? 'A club manager',
        trim(($player['first_name'] ?? '') . ' ' . ($player['last_name'] ?? '')),
        $sellingClub['name'] ?? 'the selling club'
    );
    $storyBody = sprintf(
        '%s of %s has registered transfer interest in %s, currently with %s.%s',
        $actor['full_name'] ?? 'A club manager',
        $buyingClub['name'] ?? 'the buying club',
        trim(($player['first_name'] ?? '') . ' ' . ($player['last_name'] ?? '')),
        $sellingClub['name'] ?? 'the selling club',
        $payload['interest_message'] !== '' ? ' Note: ' . $payload['interest_message'] : ''
    );

    db()->beginTransaction();

    try {
        db_query(
            'INSERT INTO transfers (
                player_id, selling_club_id, buying_club_id, initiated_by_user_id, window_id, current_stage,
                market_visibility, interest_message, created_at, updated_at
             ) VALUES (
                :player_id, :selling_club_id, :buying_club_id, :initiated_by_user_id, :window_id, :current_stage,
                :market_visibility, :interest_message, NOW(), NOW()
             )',
            [
                'player_id' => (int) $payload['player_id'],
                'selling_club_id' => (int) $payload['selling_club_id'],
                'buying_club_id' => (int) $payload['buying_club_id'],
                'initiated_by_user_id' => (int) ($actor['id'] ?? 0),
                'window_id' => $payload['window_id'],
                'current_stage' => 'interest_registered',
                'market_visibility' => 'open',
                'interest_message' => $payload['interest_message'] !== '' ? $payload['interest_message'] : null,
            ]
        );

        $transferId = (int) db()->lastInsertId();

        foreach (['selling_club', 'buying_club', 'league'] as $approvalType) {
            db_query(
                'INSERT INTO transfer_approvals (transfer_id, approval_type, status)
                 VALUES (:transfer_id, :approval_type, :status)',
                [
                    'transfer_id' => $transferId,
                    'approval_type' => $approvalType,
                    'status' => 'pending',
                ]
            );
        }

        $newsPostId = transfer_publish_news($headline, $storyBody);

        db_query(
            'INSERT INTO transfer_timeline_events (transfer_id, event_type, headline, body, publish_news, news_post_id, created_by_user_id, created_at)
             VALUES (:transfer_id, :event_type, :headline, :body, :publish_news, :news_post_id, :created_by_user_id, NOW())',
            [
                'transfer_id' => $transferId,
                'event_type' => 'interest_registered',
                'headline' => $headline,
                'body' => $storyBody,
                'publish_news' => 1,
                'news_post_id' => $newsPostId,
                'created_by_user_id' => (int) ($actor['id'] ?? 0) ?: null,
            ]
        );

        transfer_create_notifications_for_club($transferId, (int) $payload['selling_club_id'], 'New transfer interest received', $headline);

        audit_log('register_transfer_interest', 'transfers', $transferId, null, [
            'player_id' => (int) $payload['player_id'],
            'selling_club_id' => (int) $payload['selling_club_id'],
            'buying_club_id' => (int) $payload['buying_club_id'],
            'window_id' => $payload['window_id'],
            'interest_message' => $payload['interest_message'],
            'news_post_headline' => $headline,
        ]);

        db()->commit();

        return $transferId;
    } catch (Throwable $exception) {
        if (db()->inTransaction()) {
            db()->rollBack();
        }

        throw $exception;
    }
}

function transfer_phase_notes(): array
{
    return [
        'The final medical step after approval allows the buying club to complete or fail the move.',
        'Successful medical now finalizes the transfer, records the effective date, and moves the player to the buying club.',
        'Medical outcomes now publish transfer desk updates and notify both clubs automatically.',
        'Further polish, such as richer registration transitions or document attachments for medical evidence, remains available for later refinement.',
    ];
}
