<?php

namespace App\Support;

use Carbon\Carbon;

final class FinanceListOrdering
{
    /**
     * Newest first (top of list): calendar day (desc), then created time (desc), then tie-break.
     * Implemented with explicit usort so ordering does not depend on arsort/tuple behaviour.
     *
     * @param  list<array<string, mixed>>  $tx
     * @return list<array<string, mixed>>
     */
    public static function transactionsNewestFirst(array $tx): array
    {
        $copy = array_values($tx);
        usort(
            $copy,
            static function (array $a, array $b): int {
                $dayA = self::transactionDayStartTs((string) ($a['date'] ?? ''));
                $dayB = self::transactionDayStartTs((string) ($b['date'] ?? ''));
                if ($dayA !== $dayB) {
                    return $dayB <=> $dayA;
                }

                $createdA = (int) ($a['_createdTs'] ?? 0);
                $createdB = (int) ($b['_createdTs'] ?? 0);
                if ($createdA !== $createdB) {
                    return $createdB <=> $createdA;
                }

                return self::transactionTieBreakDescending($b) <=> self::transactionTieBreakDescending($a);
            }
        );

        return $copy;
    }

    /**
     * Nearest future calendar day first, then within the same day newest-created first.
     * Used for the “Upcoming” block on the transactions list.
     *
     * @param  list<array<string, mixed>>  $tx
     * @return list<array<string, mixed>>
     */
    public static function transactionsUpcomingChronological(array $tx): array
    {
        $copy = array_values($tx);
        usort(
            $copy,
            static function (array $a, array $b): int {
                $dayA = self::transactionDayStartTs((string) ($a['date'] ?? ''));
                $dayB = self::transactionDayStartTs((string) ($b['date'] ?? ''));
                if ($dayA !== $dayB) {
                    return $dayA <=> $dayB;
                }

                $createdA = (int) ($a['_createdTs'] ?? 0);
                $createdB = (int) ($b['_createdTs'] ?? 0);
                if ($createdA !== $createdB) {
                    return $createdB <=> $createdA;
                }

                return self::transactionTieBreakDescending($b) <=> self::transactionTieBreakDescending($a);
            }
        );

        return $copy;
    }

    /** Start-of-day timestamp for the transaction's calendar date (app timezone). */
    public static function transactionDayStartTs(string $date): int
    {
        if (trim($date) === '') {
            return 0;
        }
        try {
            return (int) Carbon::parse($date)->startOfDay()->timestamp;
        } catch (\Throwable) {
            return 0;
        }
    }

    /**
     * @param  list<array<string, mixed>>  $subs
     * @return list<array<string, mixed>>
     */
    public static function subscriptionsByNextRenewal(array $subs): array
    {
        $copy = array_values($subs);
        usort($copy, function (array $a, array $b): int {
            $ta = self::subscriptionRenewalSortValue($a);
            $tb = self::subscriptionRenewalSortValue($b);
            if ($ta !== $tb) {
                return $ta <=> $tb;
            }

            return self::subscriptionStatusRank($a) <=> self::subscriptionStatusRank($b)
                ?: strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
        });

        return $copy;
    }

    /**
     * @param  list<array<string, mixed>>  $budgets
     * @return list<array<string, mixed>>
     */
    public static function budgetsByUrgency(array $budgets): array
    {
        $copy = array_values($budgets);
        usort($copy, function (array $a, array $b): int {
            $ra = self::budgetUtilization($a);
            $rb = self::budgetUtilization($b);
            if (abs($ra - $rb) > 0.0001) {
                return $rb <=> $ra;
            }

            return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
        });

        return $copy;
    }

    /**
     * @param  list<array<string, mixed>>  $goals
     * @return list<array<string, mixed>>
     */
    public static function goalsByDeadline(array $goals): array
    {
        $copy = array_values($goals);
        usort($copy, function (array $a, array $b): int {
            $da = self::goalDeadlineSortValue($a);
            $db = self::goalDeadlineSortValue($b);
            if ($da !== $db) {
                return $da <=> $db;
            }

            return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
        });

        return $copy;
    }

    /**
     * @param  list<array<string, mixed>>  $rows
     * @return list<array<string, mixed>>
     */
    public static function importRowsNewestFirst(array $rows): array
    {
        $copy = array_values($rows);
        usort($copy, function (array $a, array $b): int {
            $ta = self::parseDateEndOfDayTs((string) ($a['date'] ?? ''));
            $tb = self::parseDateEndOfDayTs((string) ($b['date'] ?? ''));
            if ($ta !== $tb) {
                return $tb <=> $ta;
            }

            return self::transactionTieBreakDescending($b) <=> self::transactionTieBreakDescending($a);
        });

        return $copy;
    }

    /** ISO date string for lexicographic descending sort (invalid → last). */
    protected static function transactionIsoDateKey(string $date): string
    {
        if (trim($date) === '') {
            return '0000-00-00';
        }
        try {
            return Carbon::parse($date)->toDateString();
        } catch (\Throwable) {
            return '0000-00-00';
        }
    }

    /** Larger = newer / later in list when dates match. */
    protected static function transactionTieBreakDescending(array $t): int
    {
        $id = (string) ($t['id'] ?? '');
        if (preg_match('/^t(\d+)$/', $id, $m)) {
            return (int) $m[1];
        }
        if ($id !== '' && ctype_digit($id)) {
            return (int) $id;
        }
        $internal = $t['_internalId'] ?? null;
        if (is_numeric($internal)) {
            return (int) $internal;
        }

        return (int) sprintf('%u', crc32($id));
    }

    /** @param  array<string, mixed>  $sub */
    protected static function subscriptionRenewalSortValue(array $sub): int
    {
        $raw = $sub['nextRenewal'] ?? null;
        if ($raw === null || $raw === '') {
            return PHP_INT_MAX;
        }
        try {
            return (int) Carbon::parse((string) $raw)->startOfDay()->timestamp;
        } catch (\Throwable) {
            return PHP_INT_MAX - 1;
        }
    }

    /** @param  array<string, mixed>  $sub */
    protected static function subscriptionStatusRank(array $sub): int
    {
        return match ((string) ($sub['status'] ?? '')) {
            'active' => 0,
            'trial' => 1,
            'zombie' => 2,
            default => 3,
        };
    }

    /** @param  array<string, mixed>  $b */
    protected static function budgetUtilization(array $b): float
    {
        $limit = (float) ($b['limit'] ?? 0);
        if ($limit <= 0) {
            return 0.0;
        }

        return (float) ($b['spent'] ?? 0) / $limit;
    }

    /** @param  array<string, mixed>  $g */
    protected static function goalDeadlineSortValue(array $g): int
    {
        $raw = $g['deadline'] ?? null;
        if ($raw === null || $raw === '') {
            return PHP_INT_MAX;
        }
        try {
            return (int) Carbon::parse((string) $raw)->startOfDay()->timestamp;
        } catch (\Throwable) {
            return PHP_INT_MAX - 1;
        }
    }

    protected static function parseDateEndOfDayTs(string $date): int
    {
        if (trim($date) === '') {
            return 0;
        }
        try {
            return (int) Carbon::parse($date)->endOfDay()->timestamp;
        } catch (\Throwable) {
            return 0;
        }
    }
}
