public function getAppsFlyerDailyFunnel(Request $request)
{
    /*
    |--------------------------------------------------------------------------
    | TOKEN
    |--------------------------------------------------------------------------
    */

    $xtoken = $request->header('x-token');

    if ($xtoken !== 'iEdYTiNPc-05gBYK7DBsRxaUSCrnapqVIPrZnkJrN9Y') {
        return response()->json([
            'success' => false,
            'error' => 'miss token x-token',
        ], 403);
    }

    /*
    |--------------------------------------------------------------------------
    | TIMEZONE METIER
    |--------------------------------------------------------------------------
    */

    $businessTimezone = 'Africa/Casablanca';

    /*
    |--------------------------------------------------------------------------
    | INPUTS
    |--------------------------------------------------------------------------
    */

    $clientId = $request->input('client_id')
        ?? $request->input('client');

    $startDateInput = $request->input('start_date')
        ?? $request->input('since');

    $endDateInput = $request->input('end_date')
        ?? $request->input('until');

    /*
    |--------------------------------------------------------------------------
    | PREVIOUS PERIOD INPUTS
    |--------------------------------------------------------------------------
    |
    | La période précédente est optionnelle.
    | Si elle est fournie, les deux dates sont obligatoires.
    | Le flag interne _skip_previous empêche la récursion infinie lorsque
    | la fonction se rappelle elle-même pour calculer uniquement le total.
    |--------------------------------------------------------------------------
    */

    $skipPrevious = (bool) $request->attributes->get(
        '_skip_previous',
        false
    );

    $previousSinceInput = $request->input('previous_since');
    $previousUntilInput = $request->input('previous_until');

    $hasPreviousPeriod =
        !$skipPrevious
        && (
            $previousSinceInput !== null
            || $previousUntilInput !== null
        );

    if (
        $hasPreviousPeriod
        && (
            !$previousSinceInput
            || !$previousUntilInput
        )
    ) {
        return response()->json([
            'success' => false,
            'error' => 'previous_since et previous_until doivent être fournis ensemble',
        ], 422);
    }

    if (!$clientId || !$startDateInput || !$endDateInput) {
        return response()->json([
            'success' => false,
            'error' => 'client_id, start_date et end_date sont obligatoires',
        ], 422);
    }

    /*
    |--------------------------------------------------------------------------
    | VALIDATION DATES
    |--------------------------------------------------------------------------
    */

    try {
        $startDate = \Carbon\Carbon::parse(
            $startDateInput,
            $businessTimezone
        )->startOfDay();

        $endDate = \Carbon\Carbon::parse(
            $endDateInput,
            $businessTimezone
        )->startOfDay();

    } catch (\Throwable $e) {
        return response()->json([
            'success' => false,
            'error' => 'Format de date invalide. Format attendu : Y-m-d',
        ], 422);
    }

    if ($startDate->greaterThan($endDate)) {
        return response()->json([
            'success' => false,
            'error' => 'start_date doit être inférieure ou égale à end_date',
        ], 422);
    }

    $since = $startDate->format('Y-m-d');
    $until = $endDate->format('Y-m-d');

    $previousSince = null;
    $previousUntil = null;

    if ($hasPreviousPeriod) {
        try {
            $previousStartDate = \Carbon\Carbon::parse(
                $previousSinceInput,
                $businessTimezone
            )->startOfDay();

            $previousEndDate = \Carbon\Carbon::parse(
                $previousUntilInput,
                $businessTimezone
            )->startOfDay();
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'error' => 'Format previous_since/previous_until invalide. Format attendu : Y-m-d',
            ], 422);
        }

        if ($previousStartDate->greaterThan($previousEndDate)) {
            return response()->json([
                'success' => false,
                'error' => 'previous_since doit être inférieure ou égale à previous_until',
            ], 422);
        }

        $previousSince = $previousStartDate->format('Y-m-d');
        $previousUntil = $previousEndDate->format('Y-m-d');
    }

    /*
    |--------------------------------------------------------------------------
    | DATES ATTENDUES
    |--------------------------------------------------------------------------
    */

    $expectedDates = [];

    foreach (
        \Carbon\CarbonPeriod::create(
            $startDate->copy(),
            $endDate->copy()
        ) as $periodDay
    ) {
        $expectedDates[] = $periodDay->format('Y-m-d');
    }

    $expectedDays = count($expectedDates);

    /*
    |--------------------------------------------------------------------------
    | CLIENT
    |--------------------------------------------------------------------------
    */

    $client = Client::find($clientId);

    if (!$client) {
        return response()->json([
            'success' => false,
            'error' => 'Client introuvable',
        ], 404);
    }

    if (!$client->act_id) {
        return response()->json([
            'success' => false,
            'error' => 'Compte publicitaire Meta Ads introuvable',
        ], 404);
    }

    /*
    |--------------------------------------------------------------------------
    | META ACCOUNT
    |--------------------------------------------------------------------------
    */

    $metaAdAccountId = trim((string) $client->act_id);

    if (!str_starts_with($metaAdAccountId, 'act_')) {
        $metaAdAccountId = 'act_' . $metaAdAccountId;
    }

    /*
    |--------------------------------------------------------------------------
    | GOOGLE ADS ACCOUNTS
    |--------------------------------------------------------------------------
    */

    $managerCustomerId = 9393898970;
    $googleCustomerId = 2758763982;

    /*
    |--------------------------------------------------------------------------
    | META CAMPAIGN FILTER
    |--------------------------------------------------------------------------
    */

    $metaCampaignIds = $request->input(
        'meta_campaign_ids',
        []
    );

    $metaCampaignId = $request->input(
        'meta_campaign_id'
    );

    if ($metaCampaignId) {
        $metaCampaignIds[] = $metaCampaignId;
    }

    if (is_string($metaCampaignIds)) {
        $metaCampaignIds = explode(',', $metaCampaignIds);
    }

    $metaCampaignIds = array_values(
        array_filter(
            array_unique(
                array_map(
                    static function ($id) {
                        return trim((string) $id);
                    },
                    $metaCampaignIds
                )
            ),
            static function ($id) {
                return $id !== '';
            }
        )
    );

    /*
    |--------------------------------------------------------------------------
    | GOOGLE CAMPAIGN FILTER
    |--------------------------------------------------------------------------
    */

    $googleCampaignIds = $request->input(
        'google_campaign_ids',
        []
    );

    $googleCampaignId = $request->input(
        'google_campaign_id'
    );

    if ($googleCampaignId) {
        $googleCampaignIds[] = $googleCampaignId;
    }

    if (is_string($googleCampaignIds)) {
        $googleCampaignIds = explode(',', $googleCampaignIds);
    }

    $googleCampaignIds = array_values(
        array_filter(
            array_unique(
                array_map(
                    static function ($id) {
                        return (int) trim((string) $id);
                    },
                    $googleCampaignIds
                )
            ),
            static function ($id) {
                return $id > 0;
            }
        )
    );

    sort($metaCampaignIds);
    sort($googleCampaignIds);

    /*
    |--------------------------------------------------------------------------
    | ENDPOINT CACHE V5
    |--------------------------------------------------------------------------
    */

    $endpointCacheKey =
        'reporting:daily-funnel:v6:'
        . md5(json_encode([
            'client_id' => (int) $clientId,
            'since' => $since,
            'until' => $until,
            'previous_since' => $previousSince,
            'previous_until' => $previousUntil,
            'timezone' => $businessTimezone,
            'meta_campaign_ids' => $metaCampaignIds,
            'google_campaign_ids' => $googleCampaignIds,
        ]));

    $forceRefresh = (bool) $request->boolean(
        'refresh',
        false
    );

    if (!$forceRefresh) {
        $cachedPayload =
            \Illuminate\Support\Facades\Cache::get(
                $endpointCacheKey
            );

        if (is_array($cachedPayload)) {
            $cachedPayload['debug']['endpoint_cache_hit'] = true;

            return response()->json($cachedPayload);
        }
    }

    /*
    |--------------------------------------------------------------------------
    | EVENTS APPSFLYER
    |--------------------------------------------------------------------------
    */

    $appsflyerEventNames = [
        'onboarding_start',
        'onboarding_success',
    ];

    /*
    |--------------------------------------------------------------------------
    | DEBUG
    |--------------------------------------------------------------------------
    */

    $metaRowsCount = 0;
    $googleRowsCount = 0;

    $appsflyerRowsCount = 0;
    $appsflyerCachedReportsCount = 0;
    $appsflyerApiReportsCount = 0;
    $appsflyerSkippedRowsCount = 0;
    $appsflyerMissingUserIdCount = 0;

    $appsflyerWarnings = [];
    $appsflyerUnavailableReports = [];

    $sourceWarnings = [
        'meta' => [],
        'google' => [],
        'appsflyer' => [],
    ];

    /*
    |--------------------------------------------------------------------------
    | DATES OBSERVEES
    |--------------------------------------------------------------------------
    */

    $observedDates = [
        'meta' => [],
        'google' => [],
        'appsflyer' => [],
    ];

    /*
    |--------------------------------------------------------------------------
    | DISPONIBILITE SOURCES
    |--------------------------------------------------------------------------
    |
    | Pour Meta / Google :
    | si l'appel global réussit, la source couvre la période.
    |
    | Pour AppsFlyer :
    | suivi détaillé rapport par rapport.
    |--------------------------------------------------------------------------
    */

    $sourceCoverage = [
        'meta' => [],
        'google' => [],
        'appsflyer' => [],
    ];

    foreach ($expectedDates as $date) {
        $sourceCoverage['meta'][$date] = 'unavailable';
        $sourceCoverage['google'][$date] = 'unavailable';

        $sourceCoverage['appsflyer'][$date] = [
            'android_non_organic' => 'unavailable',
            'android_organic' => 'unavailable',
            'ios_non_organic' => 'unavailable',
            'ios_organic' => 'unavailable',
            'installs_android' => 'unavailable',
            'installs_ios' => 'unavailable',
        ];
    }

    /*
    |--------------------------------------------------------------------------
    | LAZY DAILY DATA
    |--------------------------------------------------------------------------
    |
    | IMPORTANT :
    | On ne crée plus automatiquement toutes les dates.
    |
    | Une ligne data[] est créée uniquement lorsqu'au moins une source
    | a réellement renvoyé une ligne pour cette date.
    |--------------------------------------------------------------------------
    */

    $dailyData = [];

    $createEmptyDay = static function (string $date): array {
        return [
            'date' => $date,

            'meta_spend' => 0,
            'google_spend' => 0,
            'total_spend' => 0,

            'meta_impressions' => 0,
            'google_impressions' => 0,
            'total_paid_impressions' => 0,

            'meta_clicks' => 0,
            'google_clicks' => 0,

            'ctr_meta' => 0,
            'ctr_google' => 0,

            'appsflyer_installs_android' => 0,
            'appsflyer_installs_ios' => 0,
            'appsflyer_installs_total' => 0,
            'appsflyer_installs_unattributed_os' => 0,

            'onboarding_start_active_users' => 0,
            'onboarding_success_active_users' => 0,

            'onboarding_start_active_users_android' => 0,
            'onboarding_start_active_users_ios' => 0,
            'onboarding_start_active_users_unattributed_os' => 0,

            'onboarding_success_active_users_android' => 0,
            'onboarding_success_active_users_ios' => 0,
            'onboarding_success_active_users_unattributed_os' => 0,

            /*
             * Alias dashboard existants.
             */
            'installations' => 0,
            'comptes' => 0,

            'onboarding_conversion_rate' => 0,
            'cpi' => 0,

            'currency' => 'MAD',

            'source_status' => [
                'meta' => 'unavailable',
                'google' => 'unavailable',
                'appsflyer' => 'unavailable',
            ],
        ];
    };

    $ensureDay = static function (
        string $date
    ) use (
        &$dailyData,
        $createEmptyDay,
        $expectedDates
    ) {
        if (!in_array($date, $expectedDates, true)) {
            return false;
        }

        if (!isset($dailyData[$date])) {
            $dailyData[$date] = $createEmptyDay($date);
        }

        return true;
    };

    /*
    |--------------------------------------------------------------------------
    | APPSFLYER NETWORK BUDGET
    |--------------------------------------------------------------------------
    */

    $appsflyerStartedAt = microtime(true);

    $appsflyerNetworkBudgetSeconds =
        (int) env(
            'APPSFLYER_NETWORK_BUDGET_SECONDS',
            45
        );

    $metaAccountName = null;
    $metaAccountCurrency = null;
    $googleAccountCurrency = null;

    // Nombre de campagnes actuellement actives
    $metaActiveCampaignsCount = 0;
    $googleActiveCampaignsCount = 0;

    try {

        /*
        |--------------------------------------------------------------------------
        | FX
        |--------------------------------------------------------------------------
        */

        $fx = [
            'EUR_TO_MAD' =>
                (float) env('FX_EUR_TO_MAD', 11),

            'USD_TO_MAD' =>
                (float) env('FX_USD_TO_MAD', 10.4),

            'MAD_TO_MAD' => 1,
        ];

        $convertToMad = static function (
            float $amount,
            string $fromCurrency
        ) use ($fx): float {

            $fromCurrency = strtoupper(
                trim($fromCurrency)
            );

            if ($fromCurrency === 'MAD') {
                return $amount;
            }

            $conversionKey =
                $fromCurrency . '_TO_MAD';

            if (
                !array_key_exists($conversionKey, $fx)
                || $fx[$conversionKey] <= 0
            ) {
                throw new \RuntimeException(
                    "Taux de conversion invalide ou introuvable : {$conversionKey}"
                );
            }

            return $amount * $fx[$conversionKey];
        };

        /*
        |--------------------------------------------------------------------------
        | META ADS
        |--------------------------------------------------------------------------
        */

        try {
            $metaAccessToken = $this->accessToken;

            if (!$metaAccessToken) {
                throw new \RuntimeException(
                    'Access token Meta Ads introuvable'
                );
            }

            $graphVersion = env(
                'META_GRAPH_VERSION',
                'v25.0'
            );

            /*
            |--------------------------------------------------------------------------
            | META ACCOUNT INFO
            |--------------------------------------------------------------------------
            */

            $metaAccountResponse =
                \Illuminate\Support\Facades\Http::connectTimeout(5)
                    ->timeout(30)
                    ->get(
                        "https://graph.facebook.com/{$graphVersion}/{$metaAdAccountId}",
                        [
                            'fields' => 'id,name,currency',
                            'access_token' => $metaAccessToken,
                        ]
                    );

            $metaAccountJson =
                $metaAccountResponse->json();

            if (
                $metaAccountResponse->failed()
                || isset($metaAccountJson['error'])
            ) {
                throw new \RuntimeException(
                    $metaAccountJson['error']['message']
                        ?? 'Impossible de récupérer le compte Meta Ads'
                );
            }

            $metaAccountName =
                $metaAccountJson['name']
                ?? null;

            $metaAccountCurrency = strtoupper(
                $metaAccountJson['currency']
                    ?? 'EUR'
            );

            /*
            |--------------------------------------------------------------------------
            | META ACTIVE CAMPAIGNS COUNT
            |--------------------------------------------------------------------------
            */

            try {
                $metaActiveCampaignsCount = 0;

                $metaCampaignsUrl =
                    "https://graph.facebook.com/"
                    . "{$graphVersion}/{$metaAdAccountId}/campaigns";

                $metaCampaignsParams = [
                    'fields' => 'id,effective_status',
                    'effective_status' => json_encode(['ACTIVE']),
                    'limit' => 500,
                    'access_token' => $metaAccessToken,
                ];

                do {
                    $metaCampaignsResponse =
                        \Illuminate\Support\Facades\Http::connectTimeout(5)
                            ->timeout(30)
                            ->get(
                                $metaCampaignsUrl,
                                $metaCampaignsParams
                            );

                    $metaCampaignsJson =
                        $metaCampaignsResponse->json();

                    if (
                        $metaCampaignsResponse->failed()
                        || isset($metaCampaignsJson['error'])
                    ) {
                        throw new \RuntimeException(
                            $metaCampaignsJson['error']['message']
                                ?? 'Impossible de récupérer les campagnes Meta actives'
                        );
                    }

                    foreach (
                        ($metaCampaignsJson['data'] ?? [])
                        as $campaign
                    ) {
                        if (
                            ($campaign['effective_status'] ?? null)
                            === 'ACTIVE'
                        ) {
                            $metaActiveCampaignsCount++;
                        }
                    }

                    $nextMetaCampaignsUrl =
                        $metaCampaignsJson['paging']['next']
                        ?? null;

                    if ($nextMetaCampaignsUrl) {
                        $metaCampaignsUrl = $nextMetaCampaignsUrl;
                        $metaCampaignsParams = [];
                    } else {
                        $metaCampaignsUrl = null;
                    }

                } while ($metaCampaignsUrl);

            } catch (\Throwable $e) {
                // Ne bloque pas les Meta Insights si le comptage échoue.
                $metaActiveCampaignsCount = 0;

                $sourceWarnings['meta'][] =
                    'Comptage campagnes Meta actives : ' . $e->getMessage();

                \Log::warning(
                    'META ACTIVE CAMPAIGNS COUNT ERROR',
                    [
                        'client_id' => $clientId,
                        'message' => $e->getMessage(),
                    ]
                );
            }

            /*
            |--------------------------------------------------------------------------
            | META INSIGHTS
            |--------------------------------------------------------------------------
            */

            $hasMetaCampaignFilter =
                !empty($metaCampaignIds);

            $metaFields = [
                'spend',
                'impressions',
                'clicks',
                'date_start',
                'date_stop',
            ];

            if ($hasMetaCampaignFilter) {
                $metaFields[] = 'campaign_id';
                $metaFields[] = 'campaign_name';
            }

            $metaParams = [
                'level' =>
                    $hasMetaCampaignFilter
                        ? 'campaign'
                        : 'account',

                'fields' =>
                    implode(',', $metaFields),

                'time_range' =>
                    json_encode([
                        'since' => $since,
                        'until' => $until,
                    ]),

                'time_increment' => 1,
                'limit' => 500,
                'access_token' => $metaAccessToken,
            ];

            if ($hasMetaCampaignFilter) {
                $metaParams['filtering'] =
                    json_encode([
                        [
                            'field' => 'campaign.id',
                            'operator' => 'IN',
                            'value' => $metaCampaignIds,
                        ],
                    ]);
            }

            $metaUrl =
                "https://graph.facebook.com/"
                . "{$graphVersion}/{$metaAdAccountId}/insights";

            do {
                $metaResponse =
                    \Illuminate\Support\Facades\Http::connectTimeout(5)
                        ->timeout(30)
                        ->get(
                            $metaUrl,
                            $metaParams
                        );

                $metaJson = $metaResponse->json();

                if (
                    $metaResponse->failed()
                    || isset($metaJson['error'])
                ) {
                    throw new \RuntimeException(
                        $metaJson['error']['message']
                            ?? 'Erreur Meta Ads Insights'
                    );
                }

                foreach (
                    ($metaJson['data'] ?? [])
                    as $metaRow
                ) {
                    $metaRowsCount++;

                    $date =
                        $metaRow['date_start']
                        ?? null;

                    if (!$date) {
                        continue;
                    }

                    if (!$ensureDay($date)) {
                        continue;
                    }

                    $observedDates['meta'][$date] = true;

                    $originalSpend =
                        (float) (
                            $metaRow['spend']
                            ?? 0
                        );

                    $spendMad =
                        $convertToMad(
                            $originalSpend,
                            $metaAccountCurrency
                        );

                    $dailyData[$date]['meta_spend']
                        += $spendMad;

                    $dailyData[$date]['meta_impressions']
                        += (int) (
                            $metaRow['impressions']
                            ?? 0
                        );

                    $dailyData[$date]['meta_clicks']
                        += (int) (
                            $metaRow['clicks']
                            ?? 0
                        );
                }

                $nextMetaUrl =
                    $metaJson['paging']['next']
                    ?? null;

                if ($nextMetaUrl) {
                    $metaUrl = $nextMetaUrl;

                    /*
                     * paging.next contient déjà les paramètres.
                     */
                    $metaParams = [];
                } else {
                    $metaUrl = null;
                }

            } while ($metaUrl);

            /*
             * La requête a réussi pour toute la période.
             * Une absence de ligne peut correspondre à une journée à 0.
             */
            foreach ($expectedDates as $date) {
                $sourceCoverage['meta'][$date] =
                    'complete';
            }

        } catch (\Throwable $e) {
            $sourceWarnings['meta'][] =
                $e->getMessage();

            \Log::warning(
                'DAILY FUNNEL META ERROR',
                [
                    'client_id' => $clientId,
                    'message' => $e->getMessage(),
                ]
            );
        }

        /*
        |--------------------------------------------------------------------------
        | GOOGLE ADS
        |--------------------------------------------------------------------------
        */

        try {
            $oauth2Credential =
                (new OAuth2TokenBuilder())
                    ->withClientId($this->clientId)
                    ->withClientSecret($this->clientSecret)
                    ->withRefreshToken($this->refreshToken)
                    ->build();

            $googleAdsClient =
                (new GoogleAdsClientBuilder())
                    ->withDeveloperToken(
                        $this->developerToken
                    )
                    ->withOAuth2Credential(
                        $oauth2Credential
                    )
                    ->withLoginCustomerId(
                        (int) $managerCustomerId
                    )
                    ->build();

            $googleAdsService =
                $googleAdsClient
                    ->getGoogleAdsServiceClient();

            /*
            |--------------------------------------------------------------------------
            | GOOGLE ACTIVE CAMPAIGNS COUNT
            |--------------------------------------------------------------------------
            */

            try {
                $googleActiveCampaignsCount = 0;

                $activeCampaignsQuery = "
                    SELECT
                        campaign.id
                    FROM campaign
                    WHERE campaign.status = ENABLED
                ";

                $activeCampaignsResponse =
                    $googleAdsService->search(
                        SearchGoogleAdsRequest::build(
                            (string) $googleCustomerId,
                            $activeCampaignsQuery
                        )
                    );

                foreach (
                    $activeCampaignsResponse->iterateAllElements()
                    as $campaignRow
                ) {
                    $googleActiveCampaignsCount++;
                }

            } catch (\Throwable $e) {
                // Ne bloque pas les autres métriques Google Ads.
                $googleActiveCampaignsCount = 0;

                $sourceWarnings['google'][] =
                    'Comptage campagnes Google actives : ' . $e->getMessage();

                \Log::warning(
                    'GOOGLE ACTIVE CAMPAIGNS COUNT ERROR',
                    [
                        'client_id' => $clientId,
                        'message' => $e->getMessage(),
                    ]
                );
            }

            /*
            |--------------------------------------------------------------------------
            | GOOGLE CURRENCY
            |--------------------------------------------------------------------------
            */

            $googleAccountCurrency = 'EUR';

            $currencyQuery = "
                SELECT
                    customer.currency_code
                FROM customer
                LIMIT 1
            ";

            $currencyResponse =
                $googleAdsService->search(
                    SearchGoogleAdsRequest::build(
                        (string) $googleCustomerId,
                        $currencyQuery
                    )
                );

            foreach (
                $currencyResponse->iterateAllElements()
                as $currencyRow
            ) {
                $googleAccountCurrency =
                    strtoupper(
                        (string) $currencyRow
                            ->getCustomer()
                            ->getCurrencyCode()
                    );

                break;
            }

            /*
            |--------------------------------------------------------------------------
            | GOOGLE CAMPAIGN FILTER
            |--------------------------------------------------------------------------
            */

            $googleCampaignCondition = '';

            if (!empty($googleCampaignIds)) {
                $googleIds =
                    implode(
                        ',',
                        $googleCampaignIds
                    );

                $googleCampaignCondition =
                    "AND campaign.id IN ({$googleIds})";
            }

            /*
            |--------------------------------------------------------------------------
            | GOOGLE DAILY QUERY
            |--------------------------------------------------------------------------
            */

            $googleQuery = "
                SELECT
                    segments.date,
                    metrics.cost_micros,
                    metrics.impressions,
                    metrics.clicks
                FROM campaign
                WHERE segments.date BETWEEN '{$since}' AND '{$until}'
                {$googleCampaignCondition}
                ORDER BY segments.date ASC
            ";

            $googleResponse =
                $googleAdsService->search(
                    SearchGoogleAdsRequest::build(
                        (string) $googleCustomerId,
                        $googleQuery
                    )
                );

            foreach (
                $googleResponse->iterateAllElements()
                as $googleRow
            ) {
                $googleRowsCount++;

                $date =
                    (string) $googleRow
                        ->getSegments()
                        ->getDate();

                if (!$ensureDay($date)) {
                    continue;
                }

                $observedDates['google'][$date] = true;

                $costMicros =
                    (float) $googleRow
                        ->getMetrics()
                        ->getCostMicros();

                $originalSpend =
                    $costMicros / 1000000;

                $spendMad =
                    $convertToMad(
                        $originalSpend,
                        $googleAccountCurrency
                    );

                $dailyData[$date]['google_spend']
                    += $spendMad;

                $dailyData[$date]['google_impressions']
                    += (int) $googleRow
                        ->getMetrics()
                        ->getImpressions();

                $dailyData[$date]['google_clicks']
                    += (int) $googleRow
                        ->getMetrics()
                        ->getClicks();
            }

            foreach ($expectedDates as $date) {
                $sourceCoverage['google'][$date] =
                    'complete';
            }

        } catch (\Throwable $e) {
            $sourceWarnings['google'][] =
                $e->getMessage();

            \Log::warning(
                'DAILY FUNNEL GOOGLE ERROR',
                [
                    'client_id' => $clientId,
                    'message' => $e->getMessage(),
                ]
            );
        }

        /*
        |--------------------------------------------------------------------------
        | APPSFLYER TOKEN
        |--------------------------------------------------------------------------
        */

        $appsflyerToken = config(
            'services.appsflyer.token',
            env(
                'APPSFLYER_TOKEN',
                env('appsflyer')
            )
        );

        if (!$appsflyerToken) {
            throw new \RuntimeException(
                'Token AppsFlyer non configuré.'
            );
        }

        /*
        |--------------------------------------------------------------------------
        | APPSFLYER APPS
        |--------------------------------------------------------------------------
        */

        $appsflyerApps = [
            [
                'app_id' => 'com.sgma.prod',
                'platform' => 'android',
            ],
            [
                'app_id' => 'id1055875915',
                'platform' => 'ios',
            ],
        ];

        /*
        |--------------------------------------------------------------------------
        | NORMALIZE CSV HEADER
        |--------------------------------------------------------------------------
        */

        $normalizeAppsFlyerHeader =
            static function ($header): string {

                $header =
                    preg_replace(
                        '/^\xEF\xBB\xBF/',
                        '',
                        (string) $header
                    );

                $header =
                    strtolower(
                        trim($header)
                    );

                $header =
                    preg_replace(
                        '/[^a-z0-9]+/',
                        '_',
                        $header
                    );

                return trim(
                    (string) $header,
                    '_'
                );
            };

        /*
        |--------------------------------------------------------------------------
        | CSV PARSER POUR PETITS RAPPORTS INSTALLS
        |--------------------------------------------------------------------------
        */

        $parseAppsFlyerCsv =
            static function (
                string $csvBody
            ) use (
                $normalizeAppsFlyerHeader
            ): array {

                $csvBody = trim($csvBody);

                if ($csvBody === '') {
                    return [];
                }

                $stream =
                    fopen(
                        'php://temp',
                        'r+'
                    );

                if ($stream === false) {
                    throw new \RuntimeException(
                        'Impossible de lire le CSV AppsFlyer.'
                    );
                }

                fwrite($stream, $csvBody);

                rewind($stream);

                $rawHeaders = fgetcsv($stream);

                if ($rawHeaders === false) {
                    fclose($stream);

                    return [];
                }

                $headers =
                    array_map(
                        $normalizeAppsFlyerHeader,
                        $rawHeaders
                    );

                $rows = [];

                while (
                    ($csvRow = fgetcsv($stream))
                    !== false
                ) {
                    if (
                        count(
                            array_filter(
                                $csvRow,
                                static fn ($value) =>
                                    trim(
                                        (string) $value
                                    ) !== ''
                            )
                        ) === 0
                    ) {
                        continue;
                    }

                    $csvRow =
                        array_pad(
                            array_slice(
                                $csvRow,
                                0,
                                count($headers)
                            ),
                            count($headers),
                            null
                        );

                    $row =
                        array_combine(
                            $headers,
                            $csvRow
                        );

                    if ($row === false) {
                        continue;
                    }

                    $normalized = [];

                    foreach (
                        $row as $column => $value
                    ) {
                        $normalized[$column] =
                            is_string($value)
                                ? trim($value)
                                : $value;
                    }

                    $rows[] = $normalized;
                }

                fclose($stream);

                return $rows;
            };

        /*
        |--------------------------------------------------------------------------
        | APPSFLYER DATE CHUNKS
        |--------------------------------------------------------------------------
        */

        $appsflyerDateChunks = [];

        $chunkStart = $startDate->copy();

        while ($chunkStart->lte($endDate)) {
            $chunkEnd =
                $chunkStart
                    ->copy()
                    ->addDays(30);

            if ($chunkEnd->gt($endDate)) {
                $chunkEnd =
                    $endDate->copy();
            }

            $appsflyerDateChunks[] = [
                'from' =>
                    $chunkStart->format('Y-m-d'),

                'to' =>
                    $chunkEnd->format('Y-m-d'),
            ];

            $chunkStart =
                $chunkEnd
                    ->copy()
                    ->addDay();
        }

        /*
        |--------------------------------------------------------------------------
        | HELPER DATES D'UN CHUNK
        |--------------------------------------------------------------------------
        */

        $getDatesBetween =
            static function (
                string $from,
                string $to
            ) use (
                $businessTimezone
            ): array {

                $dates = [];

                $fromCarbon =
                    \Carbon\Carbon::parse(
                        $from,
                        $businessTimezone
                    )->startOfDay();

                $toCarbon =
                    \Carbon\Carbon::parse(
                        $to,
                        $businessTimezone
                    )->startOfDay();

                foreach (
                    \Carbon\CarbonPeriod::create(
                        $fromCarbon,
                        $toCarbon
                    ) as $day
                ) {
                    $dates[] =
                        $day->format('Y-m-d');
                }

                return $dates;
            };

        /*
        |--------------------------------------------------------------------------
        | APPSFLYER RAW REPORTS
        |--------------------------------------------------------------------------
        */

        $appsflyerReports = [];

        foreach ($appsflyerApps as $app) {
            $appsflyerReports[] = [
                'key' =>
                    $app['platform']
                    . '_non_organic',

                'app_id' =>
                    $app['app_id'],

                'platform' =>
                    $app['platform'],

                'report' =>
                    'in_app_events_report/v5',
            ];

            $appsflyerReports[] = [
                'key' =>
                    $app['platform']
                    . '_organic',

                'app_id' =>
                    $app['app_id'],

                'platform' =>
                    $app['platform'],

                'report' =>
                    'organic_in_app_events_report/v5',
            ];
        }

        /*
        |--------------------------------------------------------------------------
        | UNIQUE USERS
        |--------------------------------------------------------------------------
        */

        $uniqueUsers = [];

        foreach ($expectedDates as $date) {
            $uniqueUsers[$date] = [
                'onboarding_start' => [
                    'android' => [],
                    'ios' => [],
                ],

                'onboarding_success' => [
                    'android' => [],
                    'ios' => [],
                ],
            ];
        }

        /*
        |--------------------------------------------------------------------------
        | APPSFLYER RAW DATA STREAMING
        |--------------------------------------------------------------------------
        */

        foreach (
            $appsflyerDateChunks
            as $dateChunk
        ) {
            $chunkFrom =
                $dateChunk['from'];

            $chunkTo =
                $dateChunk['to'];

            $chunkDates =
                $getDatesBetween(
                    $chunkFrom,
                    $chunkTo
                );

            foreach (
                $appsflyerReports
                as $reportConfig
            ) {
                $reportStatus = 'unavailable';

                $cacheHash =
                    md5(
                        $reportConfig['app_id']
                        . '|'
                        . $reportConfig['report']
                        . '|'
                        . $chunkFrom
                        . '|'
                        . $chunkTo
                        . '|'
                        . implode(
                            ',',
                            $appsflyerEventNames
                        )
                    );

                $appsflyerCacheDir =
                    storage_path(
                        'app/appsflyer_raw_cache'
                    );

                if (!is_dir($appsflyerCacheDir)) {
                    @mkdir(
                        $appsflyerCacheDir,
                        0775,
                        true
                    );
                }

                $cachePath =
                    $appsflyerCacheDir
                    . DIRECTORY_SEPARATOR
                    . $cacheHash
                    . '.csv';

                $chunkEndCarbon =
                    \Carbon\Carbon::parse(
                        $chunkTo,
                        $businessTimezone
                    )->endOfDay();

                $chunkIsPast =
                    $chunkEndCarbon->lt(
                        now($businessTimezone)
                            ->startOfDay()
                    );

                $cacheExists =
                    is_file($cachePath)
                    && filesize($cachePath) > 0;

                $cacheIsFresh = false;

                if ($cacheExists) {
                    $cacheIsFresh =
                        $chunkIsPast
                        || filemtime($cachePath)
                            >= (time() - 3600);
                }

                $tmpPath = null;
                $sourcePath = null;

                try {
                    /*
                    |--------------------------------------------------------------------------
                    | CACHE FRAIS
                    |--------------------------------------------------------------------------
                    */

                    if ($cacheIsFresh) {
                        $sourcePath = $cachePath;

                        $reportStatus = 'complete';

                        $appsflyerCachedReportsCount++;
                    } else {

                        /*
                        |--------------------------------------------------------------------------
                        | NETWORK BUDGET
                        |--------------------------------------------------------------------------
                        */

                        if (
                            (
                                microtime(true)
                                - $appsflyerStartedAt
                            )
                            >= $appsflyerNetworkBudgetSeconds
                        ) {
                            if ($cacheExists) {
                                $sourcePath = $cachePath;

                                $reportStatus = 'partial';

                                $appsflyerCachedReportsCount++;

                                $warning =
                                    "AppsFlyer {$reportConfig['key']} "
                                    . "[{$chunkFrom} -> {$chunkTo}] : "
                                    . 'budget réseau dépassé, stale cache utilisé.';

                                $appsflyerWarnings[] =
                                    $warning;

                                $sourceWarnings['appsflyer'][] =
                                    $warning;
                            } else {
                                $warning =
                                    "AppsFlyer {$reportConfig['key']} "
                                    . "[{$chunkFrom} -> {$chunkTo}] ignoré : "
                                    . 'budget réseau dépassé.';

                                $appsflyerWarnings[] =
                                    $warning;

                                $sourceWarnings['appsflyer'][] =
                                    $warning;

                                $appsflyerUnavailableReports[] = [
                                    'report' =>
                                        $reportConfig['key'],

                                    'from' =>
                                        $chunkFrom,

                                    'to' =>
                                        $chunkTo,

                                    'reason' =>
                                        'network_budget_exceeded',
                                ];

                                foreach (
                                    $chunkDates
                                    as $date
                                ) {
                                    $sourceCoverage[
                                        'appsflyer'
                                    ][$date][
                                        $reportConfig['key']
                                    ] = 'unavailable';
                                }

                                continue;
                            }
                        } else {
                            $tmpPath =
                                tempnam(
                                    sys_get_temp_dir(),
                                    'af_'
                                );

                            if ($tmpPath === false) {
                                throw new \RuntimeException(
                                    'Impossible de créer un fichier temporaire AppsFlyer.'
                                );
                            }

                            $url =
                                sprintf(
                                    'https://hq1.appsflyer.com/api/raw-data/export/app/%s/%s',
                                    rawurlencode(
                                        $reportConfig['app_id']
                                    ),
                                    $reportConfig['report']
                                );

                            $response =
                                \Illuminate\Support\Facades\Http::withToken(
                                    $appsflyerToken
                                )
                                ->accept('text/csv')
                                ->connectTimeout(
                                    (int) env(
                                        'APPSFLYER_CONNECT_TIMEOUT',
                                        5
                                    )
                                )
                                ->timeout(
                                    (int) env(
                                        'APPSFLYER_RAW_TIMEOUT',
                                        30
                                    )
                                )
                                ->withOptions([
                                    'sink' => $tmpPath,
                                ])
                                ->get(
                                    $url,
                                    [
                                        'from' =>
                                            $chunkFrom,

                                        'to' =>
                                            $chunkTo,

                                        'event_name' =>
                                            implode(
                                                ',',
                                                $appsflyerEventNames
                                            ),

                                        'maximum_rows' =>
                                            200000,

                                        'currency' =>
                                            'preferred',
                                    ]
                                );

                            $appsflyerApiReportsCount++;

                            if (!$response->successful()) {
                                $errorBody = '';

                                if (
                                    is_file($tmpPath)
                                    && filesize($tmpPath)
                                        <= 1024 * 1024
                                ) {
                                    $errorBody =
                                        trim(
                                            (string) file_get_contents(
                                                $tmpPath
                                            )
                                        );
                                }

                                $errorMessage =
                                    "AppsFlyer {$reportConfig['key']} "
                                    . "[{$chunkFrom} -> {$chunkTo}]: HTTP "
                                    . $response->status()
                                    . (
                                        $errorBody !== ''
                                            ? ' - ' . $errorBody
                                            : ''
                                    );

                                if ($cacheExists) {
                                    $sourcePath =
                                        $cachePath;

                                    $reportStatus =
                                        'partial';

                                    $appsflyerCachedReportsCount++;

                                    $appsflyerWarnings[] =
                                        $errorMessage
                                        . ' | stale cache utilisé.';

                                    $sourceWarnings[
                                        'appsflyer'
                                    ][] =
                                        $errorMessage
                                        . ' | stale cache utilisé.';
                                } else {
                                    $appsflyerWarnings[] =
                                        $errorMessage;

                                    $sourceWarnings[
                                        'appsflyer'
                                    ][] =
                                        $errorMessage;

                                    $appsflyerUnavailableReports[] = [
                                        'report' =>
                                            $reportConfig['key'],

                                        'from' =>
                                            $chunkFrom,

                                        'to' =>
                                            $chunkTo,

                                        'http_status' =>
                                            $response->status(),
                                    ];

                                    foreach (
                                        $chunkDates
                                        as $date
                                    ) {
                                        $sourceCoverage[
                                            'appsflyer'
                                        ][$date][
                                            $reportConfig['key']
                                        ] = 'unavailable';
                                    }

                                    continue;
                                }
                            } else {
                                /*
                                 * Réponse HTTP réussie.
                                 *
                                 * Un CSV vide reste une réponse disponible :
                                 * cela peut simplement signifier aucun event.
                                 */
                                $reportStatus =
                                    'complete';

                                if (
                                    is_file($tmpPath)
                                    && filesize($tmpPath) > 0
                                ) {
                                    $cacheTmpPath =
                                        $cachePath
                                        . '.tmp';

                                    @copy(
                                        $tmpPath,
                                        $cacheTmpPath
                                    );

                                    if (
                                        is_file(
                                            $cacheTmpPath
                                        )
                                    ) {
                                        @rename(
                                            $cacheTmpPath,
                                            $cachePath
                                        );
                                    }

                                    $sourcePath =
                                        $tmpPath;
                                }
                            }
                        }
                    }

                    /*
                    |--------------------------------------------------------------------------
                    | MARQUER COVERAGE
                    |--------------------------------------------------------------------------
                    */

                    foreach (
                        $chunkDates
                        as $date
                    ) {
                        $sourceCoverage[
                            'appsflyer'
                        ][$date][
                            $reportConfig['key']
                        ] = $reportStatus;
                    }

                    /*
                    |--------------------------------------------------------------------------
                    | PAS DE CSV = RAPPORT DISPONIBLE MAIS AUCUN EVENT
                    |--------------------------------------------------------------------------
                    */

                    if (
                        !$sourcePath
                        || !is_file($sourcePath)
                        || filesize($sourcePath) === 0
                    ) {
                        continue;
                    }

                    /*
                    |--------------------------------------------------------------------------
                    | STREAM CSV
                    |--------------------------------------------------------------------------
                    */

                    $handle =
                        fopen(
                            $sourcePath,
                            'r'
                        );

                    if ($handle === false) {
                        throw new \RuntimeException(
                            'Impossible d’ouvrir le CSV AppsFlyer temporaire.'
                        );
                    }

                    try {
                        $rawHeaders =
                            fgetcsv($handle);

                        if ($rawHeaders === false) {
                            continue;
                        }

                        $headers =
                            array_map(
                                $normalizeAppsFlyerHeader,
                                $rawHeaders
                            );

                        $eventNameIndex =
                            array_search(
                                'event_name',
                                $headers,
                                true
                            );

                        $eventTimeIndex =
                            array_search(
                                'event_time',
                                $headers,
                                true
                            );

                        $appsflyerIdIndex =
                            array_search(
                                'appsflyer_id',
                                $headers,
                                true
                            );

                        $customerUserIdIndex =
                            array_search(
                                'customer_user_id',
                                $headers,
                                true
                            );

                        if (
                            $eventNameIndex === false
                            || $eventTimeIndex === false
                        ) {
                            throw new \RuntimeException(
                                'Colonnes event_name/event_time absentes du rapport AppsFlyer.'
                            );
                        }

                        $rowsInCurrentReport = 0;

                        while (
                            ($csvRow = fgetcsv($handle))
                            !== false
                        ) {
                            $rowsInCurrentReport++;
                            $appsflyerRowsCount++;

                            $eventName =
                                trim(
                                    (string) (
                                        $csvRow[
                                            $eventNameIndex
                                        ]
                                        ?? ''
                                    )
                                );

                            if (
                                !in_array(
                                    $eventName,
                                    $appsflyerEventNames,
                                    true
                                )
                            ) {
                                $appsflyerSkippedRowsCount++;

                                continue;
                            }

                            $eventTime =
                                $csvRow[
                                    $eventTimeIndex
                                ]
                                ?? null;

                            if (!$eventTime) {
                                $appsflyerSkippedRowsCount++;

                                continue;
                            }

                            $date =
                                substr(
                                    (string) $eventTime,
                                    0,
                                    10
                                );

                            if (
                                strlen($date) !== 10
                                || !isset(
                                    $uniqueUsers[$date]
                                )
                            ) {
                                $appsflyerSkippedRowsCount++;

                                continue;
                            }

                            $observedDates[
                                'appsflyer'
                            ][$date] = true;

                            $ensureDay($date);

                            /*
                            |--------------------------------------------------------------------------
                            | USER ID
                            |--------------------------------------------------------------------------
                            */

                            $appsflyerId = '';

                            if (
                                $appsflyerIdIndex
                                !== false
                            ) {
                                $appsflyerId =
                                    trim(
                                        (string) (
                                            $csvRow[
                                                $appsflyerIdIndex
                                            ]
                                            ?? ''
                                        )
                                    );
                            }

                            $customerUserId = '';

                            if (
                                $customerUserIdIndex
                                !== false
                            ) {
                                $customerUserId =
                                    trim(
                                        (string) (
                                            $csvRow[
                                                $customerUserIdIndex
                                            ]
                                            ?? ''
                                        )
                                    );
                            }

                            if ($appsflyerId !== '') {
                                $uniqueUserKey =
                                    md5(
                                        'af:'
                                        . $appsflyerId,
                                        true
                                    );
                            } elseif (
                                $customerUserId !== ''
                            ) {
                                $uniqueUserKey =
                                    md5(
                                        'cuid:'
                                        . $customerUserId,
                                        true
                                    );
                            } else {
                                $appsflyerMissingUserIdCount++;

                                continue;
                            }

                            $platform =
                                $reportConfig[
                                    'platform'
                                ];

                            $uniqueUsers[
                                $date
                            ][
                                $eventName
                            ][
                                $platform
                            ][
                                $uniqueUserKey
                            ] = true;
                        }

                        if (
                            $rowsInCurrentReport
                            >= 200000
                        ) {
                            $warning =
                                "AppsFlyer {$reportConfig['key']} "
                                . "[{$chunkFrom} -> {$chunkTo}] "
                                . 'a atteint maximum_rows=200000.';

                            $appsflyerWarnings[] =
                                $warning;

                            $sourceWarnings[
                                'appsflyer'
                            ][] =
                                $warning;

                            /*
                             * Le rapport est potentiellement tronqué.
                             */
                            foreach (
                                $chunkDates
                                as $date
                            ) {
                                $sourceCoverage[
                                    'appsflyer'
                                ][$date][
                                    $reportConfig['key']
                                ] = 'partial';
                            }
                        }

                    } finally {
                        if (is_resource($handle)) {
                            fclose($handle);
                        }
                    }

                } catch (\Throwable $e) {
                    $warning =
                        "AppsFlyer {$reportConfig['key']} "
                        . "[{$chunkFrom} -> {$chunkTo}] : "
                        . $e->getMessage();

                    $appsflyerWarnings[] =
                        $warning;

                    $sourceWarnings[
                        'appsflyer'
                    ][] =
                        $warning;

                    foreach (
                        $chunkDates
                        as $date
                    ) {
                        $sourceCoverage[
                            'appsflyer'
                        ][$date][
                            $reportConfig['key']
                        ] = 'unavailable';
                    }
                } finally {
                    if (
                        isset($tmpPath)
                        && $tmpPath
                        && file_exists($tmpPath)
                    ) {
                        @unlink($tmpPath);
                    }

                    if (
                        function_exists(
                            'gc_collect_cycles'
                        )
                    ) {
                        gc_collect_cycles();
                    }
                }
            }
        }

        /*
        |--------------------------------------------------------------------------
        | APPSFLYER INSTALLS
        |--------------------------------------------------------------------------
        |
        | Attention :
        | le total actuel reste Android + iOS.
        |
        | Donc unattributed_os n'est PAS détectable avec ce rapport seul.
        |--------------------------------------------------------------------------
        */

        foreach ($appsflyerApps as $app) {
            $platform =
                $app['platform'];

            $coverageKey =
                $platform === 'android'
                    ? 'installs_android'
                    : 'installs_ios';

            $installCacheKey =
                'appsflyer:daily-installs:v2:'
                . md5(
                    $app['app_id']
                    . '|'
                    . $since
                    . '|'
                    . $until
                );

            $installCsv =
                \Illuminate\Support\Facades\Cache::get(
                    $installCacheKey
                );

            $installReportAvailable = false;

            if (is_string($installCsv)) {
                $installReportAvailable = true;

                foreach ($expectedDates as $date) {
                    $sourceCoverage[
                        'appsflyer'
                    ][$date][
                        $coverageKey
                    ] = 'complete';
                }
            } else {
                try {
                    $installUrl =
                        sprintf(
                            'https://hq1.appsflyer.com/api/agg-data/export/app/%s/partners_by_date_report/v5',
                            rawurlencode(
                                $app['app_id']
                            )
                        );

                    $installResponse =
                        \Illuminate\Support\Facades\Http::withToken(
                            $appsflyerToken
                        )
                        ->accept('text/csv')
                        ->connectTimeout(
                            (int) env(
                                'APPSFLYER_CONNECT_TIMEOUT',
                                5
                            )
                        )
                        ->timeout(
                            (int) env(
                                'APPSFLYER_INSTALLS_TIMEOUT',
                                20
                            )
                        )
                        ->get(
                            $installUrl,
                            [
                                'from' => $since,
                                'to' => $until,
                                'category' => 'standard',
                                'currency' => 'preferred',
                            ]
                        );

                    if (!$installResponse->successful()) {
                        $warning =
                            'AppsFlyer installs '
                            . $platform
                            . ': HTTP '
                            . $installResponse->status();

                        $appsflyerWarnings[] =
                            $warning;

                        $sourceWarnings[
                            'appsflyer'
                        ][] =
                            $warning;
                    } else {
                        $installCsv =
                            $installResponse->body();

                        $installReportAvailable =
                            true;

                        foreach (
                            $expectedDates
                            as $date
                        ) {
                            $sourceCoverage[
                                'appsflyer'
                            ][$date][
                                $coverageKey
                            ] = 'complete';
                        }

                        $containsToday =
                            $startDate->lte(
                                now(
                                    $businessTimezone
                                )->startOfDay()
                            )
                            && $endDate->gte(
                                now(
                                    $businessTimezone
                                )->startOfDay()
                            );

                        \Illuminate\Support\Facades\Cache::put(
                            $installCacheKey,
                            $installCsv,
                            $containsToday
                                ? now(
                                    $businessTimezone
                                )->addHours(2)
                                : now(
                                    $businessTimezone
                                )->addDays(30)
                        );
                    }

                } catch (\Throwable $e) {
                    $warning =
                        'AppsFlyer installs '
                        . $platform
                        . ': '
                        . $e->getMessage();

                    $appsflyerWarnings[] =
                        $warning;

                    $sourceWarnings[
                        'appsflyer'
                    ][] =
                        $warning;
                }
            }

            if (
                !$installReportAvailable
                || !is_string($installCsv)
            ) {
                continue;
            }

            $installRows =
                $parseAppsFlyerCsv(
                    $installCsv
                );

            foreach (
                $installRows
                as $row
            ) {
                $dateRaw =
                    $row['date']
                    ?? $row['day']
                    ?? null;

                if (!$dateRaw) {
                    continue;
                }

                try {
                    $date =
                        \Carbon\Carbon::parse(
                            $dateRaw,
                            $businessTimezone
                        )->format('Y-m-d');
                } catch (\Throwable $e) {
                    continue;
                }

                if (
                    !in_array(
                        $date,
                        $expectedDates,
                        true
                    )
                ) {
                    continue;
                }

                /*
                 * Même une ligne installs=0 signifie que
                 * AppsFlyer a fourni cette journée.
                 */
                $observedDates[
                    'appsflyer'
                ][$date] = true;

                $ensureDay($date);

                $installs =
                    (int) str_replace(
                        [',', ' '],
                        '',
                        (string) (
                            $row['installs']
                            ?? 0
                        )
                    );

                if ($platform === 'android') {
                    $dailyData[
                        $date
                    ][
                        'appsflyer_installs_android'
                    ] += $installs;
                }

                if ($platform === 'ios') {
                    $dailyData[
                        $date
                    ][
                        'appsflyer_installs_ios'
                    ] += $installs;
                }

                /*
                 * SOURCE ACTUELLE :
                 *
                 * total = android + ios
                 *
                 * Il ne s'agit PAS d'un total AppsFlyer
                 * indépendant des plateformes.
                 */
                $dailyData[
                    $date
                ][
                    'appsflyer_installs_total'
                ] += $installs;
            }
        }

        /*
        |--------------------------------------------------------------------------
        | TRANSFORMER UNIQUE SETS EN COUNTS
        |--------------------------------------------------------------------------
        */

        foreach (
            $dailyData
            as $date => &$day
        ) {
            $startAndroid =
                count(
                    $uniqueUsers[
                        $date
                    ][
                        'onboarding_start'
                    ][
                        'android'
                    ]
                );

            $startIos =
                count(
                    $uniqueUsers[
                        $date
                    ][
                        'onboarding_start'
                    ][
                        'ios'
                    ]
                );

            $successAndroid =
                count(
                    $uniqueUsers[
                        $date
                    ][
                        'onboarding_success'
                    ][
                        'android'
                    ]
                );

            $successIos =
                count(
                    $uniqueUsers[
                        $date
                    ][
                        'onboarding_success'
                    ][
                        'ios'
                    ]
                );

            /*
            |--------------------------------------------------------------------------
            | TOTAL ONBOARDING ACTUEL
            |--------------------------------------------------------------------------
            |
            | Le total est actuellement reconstruit Android + iOS.
            |--------------------------------------------------------------------------
            */

            $day[
                'onboarding_start_active_users'
            ] =
                $startAndroid
                + $startIos;

            $day[
                'onboarding_success_active_users'
            ] =
                $successAndroid
                + $successIos;

            /*
            |--------------------------------------------------------------------------
            | OS
            |--------------------------------------------------------------------------
            */

            $day[
                'onboarding_start_active_users_android'
            ] =
                $startAndroid;

            $day[
                'onboarding_start_active_users_ios'
            ] =
                $startIos;

            $day[
                'onboarding_success_active_users_android'
            ] =
                $successAndroid;

            $day[
                'onboarding_success_active_users_ios'
            ] =
                $successIos;

            /*
            |--------------------------------------------------------------------------
            | UNATTRIBUTED OS
            |--------------------------------------------------------------------------
            */

            $day[
                'appsflyer_installs_unattributed_os'
            ] =
                max(
                    0,
                    (int) $day[
                        'appsflyer_installs_total'
                    ]
                    - (int) $day[
                        'appsflyer_installs_android'
                    ]
                    - (int) $day[
                        'appsflyer_installs_ios'
                    ]
                );

            $day[
                'onboarding_start_active_users_unattributed_os'
            ] =
                max(
                    0,
                    (int) $day[
                        'onboarding_start_active_users'
                    ]
                    - (int) $day[
                        'onboarding_start_active_users_android'
                    ]
                    - (int) $day[
                        'onboarding_start_active_users_ios'
                    ]
                );

            $day[
                'onboarding_success_active_users_unattributed_os'
            ] =
                max(
                    0,
                    (int) $day[
                        'onboarding_success_active_users'
                    ]
                    - (int) $day[
                        'onboarding_success_active_users_android'
                    ]
                    - (int) $day[
                        'onboarding_success_active_users_ios'
                    ]
                );

            /*
            |--------------------------------------------------------------------------
            | ALIASES DASHBOARD
            |--------------------------------------------------------------------------
            */

            $day['installations'] =
                $day[
                    'onboarding_start_active_users'
                ];

            $day['comptes'] =
                $day[
                    'onboarding_success_active_users'
                ];

            /*
            |--------------------------------------------------------------------------
            | ONBOARDING RATE
            |--------------------------------------------------------------------------
            */

            $day[
                'onboarding_conversion_rate'
            ] =
                $day[
                    'onboarding_start_active_users'
                ] > 0
                    ? round(
                        (
                            $day[
                                'onboarding_success_active_users'
                            ]
                            /
                            $day[
                                'onboarding_start_active_users'
                            ]
                        ) * 100,
                        2
                    )
                    : 0;
        }

        unset($day);

        /*
        |--------------------------------------------------------------------------
        | UNIQUE USERS SUR TOUTE LA PERIODE
        |--------------------------------------------------------------------------
        */

        $periodUniqueUsers = [
            'onboarding_start' => [
                'android' => [],
                'ios' => [],
            ],

            'onboarding_success' => [
                'android' => [],
                'ios' => [],
            ],
        ];

        foreach (
            $uniqueUsers
            as $dateSets
        ) {
            foreach (
                $appsflyerEventNames
                as $eventName
            ) {
                foreach (
                    ['android', 'ios']
                    as $platform
                ) {
                    foreach (
                        $dateSets[
                            $eventName
                        ][
                            $platform
                        ]
                        as $userId => $true
                    ) {
                        $periodUniqueUsers[
                            $eventName
                        ][
                            $platform
                        ][
                            $userId
                        ] = true;
                    }
                }
            }
        }

        $totalOnboardingStartAndroid =
            count(
                $periodUniqueUsers[
                    'onboarding_start'
                ][
                    'android'
                ]
            );

        $totalOnboardingStartIos =
            count(
                $periodUniqueUsers[
                    'onboarding_start'
                ][
                    'ios'
                ]
            );

        $totalOnboardingSuccessAndroid =
            count(
                $periodUniqueUsers[
                    'onboarding_success'
                ][
                    'android'
                ]
            );

        $totalOnboardingSuccessIos =
            count(
                $periodUniqueUsers[
                    'onboarding_success'
                ][
                    'ios'
                ]
            );

        $totalOnboardingStartActiveUsers =
            $totalOnboardingStartAndroid
            + $totalOnboardingStartIos;

        $totalOnboardingSuccessActiveUsers =
            $totalOnboardingSuccessAndroid
            + $totalOnboardingSuccessIos;

        $totalOnboardingStartUnattributedOs =
            max(
                0,
                $totalOnboardingStartActiveUsers
                - $totalOnboardingStartAndroid
                - $totalOnboardingStartIos
            );

        $totalOnboardingSuccessUnattributedOs =
            max(
                0,
                $totalOnboardingSuccessActiveUsers
                - $totalOnboardingSuccessAndroid
                - $totalOnboardingSuccessIos
            );

        /*
        |--------------------------------------------------------------------------
        | APPSFLYER STATUS HELPER
        |--------------------------------------------------------------------------
        */

        $resolveAppsFlyerStatus =
            static function (
                array $availability
            ): string {

                if (empty($availability)) {
                    return 'unavailable';
                }

                $values =
                    array_values(
                        $availability
                    );

                $completeCount =
                    count(
                        array_filter(
                            $values,
                            static fn ($status) =>
                                $status === 'complete'
                        )
                    );

                $partialCount =
                    count(
                        array_filter(
                            $values,
                            static fn ($status) =>
                                $status === 'partial'
                        )
                    );

                $unavailableCount =
                    count(
                        array_filter(
                            $values,
                            static fn ($status) =>
                                $status === 'unavailable'
                        )
                    );

                if (
                    $completeCount === count($values)
                ) {
                    return 'complete';
                }

                if (
                    $completeCount === 0
                    && $partialCount === 0
                    && $unavailableCount
                        === count($values)
                ) {
                    return 'unavailable';
                }

                return 'partial';
            };

        /*
        |--------------------------------------------------------------------------
        | CALCUL FINAL JOURNALIER
        |--------------------------------------------------------------------------
        */

        $totalMetaSpend = 0;
        $totalGoogleSpend = 0;
        $totalCombinedSpend = 0;

        $totalMetaImpressions = 0;
        $totalGoogleImpressions = 0;
        $totalPaidImpressions = 0;

        $totalMetaClicks = 0;
        $totalGoogleClicks = 0;

        $totalAppsFlyerInstallsAndroid = 0;
        $totalAppsFlyerInstallsIos = 0;
        $totalAppsFlyerInstalls = 0;
        $totalAppsFlyerInstallsUnattributedOs = 0;

        foreach (
            $dailyData
            as $date => &$day
        ) {
            /*
            |--------------------------------------------------------------------------
            | SOURCE STATUS
            |--------------------------------------------------------------------------
            */

            $day['source_status']['meta'] =
                $sourceCoverage[
                    'meta'
                ][$date]
                ?? 'unavailable';

            $day['source_status']['google'] =
                $sourceCoverage[
                    'google'
                ][$date]
                ?? 'unavailable';

            $day['source_status']['appsflyer'] =
                $resolveAppsFlyerStatus(
                    $sourceCoverage[
                        'appsflyer'
                    ][$date]
                    ?? []
                );

            /*
            |--------------------------------------------------------------------------
            | SPEND
            |--------------------------------------------------------------------------
            */

            $day['meta_spend'] =
                round(
                    (float) $day[
                        'meta_spend'
                    ],
                    2
                );

            $day['google_spend'] =
                round(
                    (float) $day[
                        'google_spend'
                    ],
                    2
                );

            $day['total_spend'] =
                round(
                    $day['meta_spend']
                    + $day['google_spend'],
                    2
                );

            /*
            |--------------------------------------------------------------------------
            | IMPRESSIONS
            |--------------------------------------------------------------------------
            */

            $day['meta_impressions'] =
                (int) $day[
                    'meta_impressions'
                ];

            $day['google_impressions'] =
                (int) $day[
                    'google_impressions'
                ];

            $day['total_paid_impressions'] =
                $day['meta_impressions']
                + $day['google_impressions'];

            /*
            |--------------------------------------------------------------------------
            | CLICKS
            |--------------------------------------------------------------------------
            */

            $day['meta_clicks'] =
                (int) $day[
                    'meta_clicks'
                ];

            $day['google_clicks'] =
                (int) $day[
                    'google_clicks'
                ];

            /*
            |--------------------------------------------------------------------------
            | CTR
            |--------------------------------------------------------------------------
            */

            $day['ctr_meta'] =
                $day[
                    'meta_impressions'
                ] > 0
                    ? round(
                        (
                            $day['meta_clicks']
                            /
                            $day[
                                'meta_impressions'
                            ]
                        ) * 100,
                        2
                    )
                    : 0;

            $day['ctr_google'] =
                $day[
                    'google_impressions'
                ] > 0
                    ? round(
                        (
                            $day['google_clicks']
                            /
                            $day[
                                'google_impressions'
                            ]
                        ) * 100,
                        2
                    )
                    : 0;

            /*
            |--------------------------------------------------------------------------
            | CPI HISTORIQUE
            |--------------------------------------------------------------------------
            */

            $day['cpi'] =
                $day['installations'] > 0
                    ? round(
                        $day['total_spend']
                        /
                        $day['installations'],
                        2
                    )
                    : 0;

            /*
            |--------------------------------------------------------------------------
            | TOTALS
            |--------------------------------------------------------------------------
            */

            $totalMetaSpend +=
                $day['meta_spend'];

            $totalGoogleSpend +=
                $day['google_spend'];

            $totalCombinedSpend +=
                $day['total_spend'];

            $totalMetaImpressions +=
                $day['meta_impressions'];

            $totalGoogleImpressions +=
                $day['google_impressions'];

            $totalPaidImpressions +=
                $day['total_paid_impressions'];

            $totalMetaClicks +=
                $day['meta_clicks'];

            $totalGoogleClicks +=
                $day['google_clicks'];

            $totalAppsFlyerInstallsAndroid +=
                (int) $day[
                    'appsflyer_installs_android'
                ];

            $totalAppsFlyerInstallsIos +=
                (int) $day[
                    'appsflyer_installs_ios'
                ];

            $totalAppsFlyerInstalls +=
                (int) $day[
                    'appsflyer_installs_total'
                ];

            $totalAppsFlyerInstallsUnattributedOs +=
                (int) $day[
                    'appsflyer_installs_unattributed_os'
                ];
        }

        unset($day);

        /*
        |--------------------------------------------------------------------------
        | TRI
        |--------------------------------------------------------------------------
        */

        ksort($dailyData);

        /*
        |--------------------------------------------------------------------------
        | DATES RETOURNEES
        |--------------------------------------------------------------------------
        */

        $returnedDates =
            array_keys($dailyData);

        /*
        |--------------------------------------------------------------------------
        | MISSING DATES
        |--------------------------------------------------------------------------
        |
        | Une date sans AUCUNE ligne réelle de Meta / Google / AppsFlyer
        | n'existe pas dans $dailyData et apparaît donc ici.
        |--------------------------------------------------------------------------
        */

        $missingDates =
            array_values(
                array_diff(
                    $expectedDates,
                    $returnedDates
                )
            );

        /*
        |--------------------------------------------------------------------------
        | DUPLICATE DATES
        |--------------------------------------------------------------------------
        */

        $dateOccurrences = [];

        foreach (
            array_values($dailyData)
            as $row
        ) {
            $date =
                $row['date']
                ?? null;

            if (!$date) {
                continue;
            }

            $dateOccurrences[$date] =
                (
                    $dateOccurrences[$date]
                    ?? 0
                ) + 1;
        }

        $duplicateDates = [];

        foreach (
            $dateOccurrences
            as $date => $count
        ) {
            if ($count > 1) {
                $duplicateDates[] =
                    $date;
            }
        }

        /*
        |--------------------------------------------------------------------------
        | GLOBAL FUNNEL
        |--------------------------------------------------------------------------
        */

        $totalInstallations =
            $totalOnboardingStartActiveUsers;

        $totalComptes =
            $totalOnboardingSuccessActiveUsers;

        $globalOnboardingConversionRate =
            $totalOnboardingStartActiveUsers > 0
                ? round(
                    (
                        $totalOnboardingSuccessActiveUsers
                        /
                        $totalOnboardingStartActiveUsers
                    ) * 100,
                    2
                )
                : 0;

        $globalCpi =
            $totalInstallations > 0
                ? round(
                    $totalCombinedSpend
                    /
                    $totalInstallations,
                    2
                )
                : 0;

        $globalCtrMeta =
            $totalMetaImpressions > 0
                ? round(
                    (
                        $totalMetaClicks
                        /
                        $totalMetaImpressions
                    ) * 100,
                    2
                )
                : 0;

        $globalCtrGoogle =
            $totalGoogleImpressions > 0
                ? round(
                    (
                        $totalGoogleClicks
                        /
                        $totalGoogleImpressions
                    ) * 100,
                    2
                )
                : 0;

        /*
        |--------------------------------------------------------------------------
        | DEBUG SOURCE DATES
        |--------------------------------------------------------------------------
        */

        $sourceDates = [
            'meta' =>
                array_values(
                    array_keys(
                        $observedDates['meta']
                    )
                ),

            'google' =>
                array_values(
                    array_keys(
                        $observedDates['google']
                    )
                ),

            'appsflyer' =>
                array_values(
                    array_keys(
                        $observedDates['appsflyer']
                    )
                ),
        ];

        sort($sourceDates['meta']);
        sort($sourceDates['google']);
        sort($sourceDates['appsflyer']);

        /*
        |--------------------------------------------------------------------------
        | RESPONSE
        |--------------------------------------------------------------------------
        */

        $payload = [
            'success' => true,

            'client_id' =>
                (int) $clientId,

            /*
            |--------------------------------------------------------------------------
            | PERIOD
            |--------------------------------------------------------------------------
            */

            'period' => [
                /*
                 * Nouveau format.
                 */
                'since' => $since,
                'until' => $until,
                'inclusive' => true,
                'timezone' => $businessTimezone,
                'expected_days' => $expectedDays,
                'returned_days' => count($dailyData),
                'missing_dates' => $missingDates,
                'duplicate_dates' => $duplicateDates,

                /*
                 * Compatibilité ancienne API / frontend.
                 */
                'start_date' => $since,
                'end_date' => $until,
                'days_count' => $expectedDays,
            ],

            /*
            |--------------------------------------------------------------------------
            | ACCOUNTS
            |--------------------------------------------------------------------------
            */

            'accounts' => [
                'meta_ads' => [
                    'account_id' =>
                        $metaAdAccountId,

                    'account_name' =>
                        $metaAccountName,

                    'original_currency' =>
                        $metaAccountCurrency,
                ],

                'google_ads' => [
                    'manager_customer_id' =>
                        (string) $managerCustomerId,

                    'customer_id' =>
                        (string) $googleCustomerId,

                    'original_currency' =>
                        $googleAccountCurrency,
                ],

                'appsflyer' => [
                    'android_app_id' =>
                        'com.sgma.prod',

                    'ios_app_id' =>
                        'id1055875915',

                    'mode' =>
                        'raw_data_unique_users',
                ],
            ],

            /*
            |--------------------------------------------------------------------------
            | FILTERS
            |--------------------------------------------------------------------------
            */

            'filters' => [
                'meta_campaign_ids' =>
                    $metaCampaignIds,

                'google_campaign_ids' =>
                    $googleCampaignIds,

                'all_meta_campaigns' =>
                    empty($metaCampaignIds),

                'all_google_campaigns' =>
                    empty($googleCampaignIds),
            ],

            /*
            |--------------------------------------------------------------------------
            | METRIC DEFINITIONS
            |--------------------------------------------------------------------------
            */

            'metric' => [
                'spend' =>
                    'MAD',

                'onboarding_start_active_users' =>
                    'unique appsflyer_id for onboarding_start',

                'onboarding_success_active_users' =>
                    'unique appsflyer_id for onboarding_success',

                'onboarding_start_active_users_android' =>
                    'unique Android users for onboarding_start',

                'onboarding_start_active_users_ios' =>
                    'unique iOS users for onboarding_start',

                'onboarding_start_active_users_unattributed_os' =>
                    'total starts - Android starts - iOS starts',

                'onboarding_success_active_users_android' =>
                    'unique Android users for onboarding_success',

                'onboarding_success_active_users_ios' =>
                    'unique iOS users for onboarding_success',

                'onboarding_success_active_users_unattributed_os' =>
                    'total success - Android success - iOS success',

                'appsflyer_installs_android' =>
                    'partners_by_date_report/v5 installs Android',

                'appsflyer_installs_ios' =>
                    'partners_by_date_report/v5 installs iOS',

                'appsflyer_installs_total' =>
                    'Android installs + iOS installs with current implementation',

                'appsflyer_installs_unattributed_os' =>
                    'appsflyer_installs_total - android - ios',

                'meta_impressions' =>
                    'impressions',

                'google_impressions' =>
                    'metrics.impressions',

                'meta_clicks' =>
                    'clicks',

                'google_clicks' =>
                    'metrics.clicks',

                'ctr_meta' =>
                    'meta_clicks / meta_impressions × 100',

                'ctr_google' =>
                    'google_clicks / google_impressions × 100',
            ],

            /*
            |--------------------------------------------------------------------------
            | MAPPING
            |--------------------------------------------------------------------------
            */

            'mapping' => [
                'installations' =>
                    'onboarding_start_active_users',

                'comptes' =>
                    'onboarding_success_active_users',

                'onboarding_conversion_rate' =>
                    'onboarding_success_active_users / onboarding_start_active_users × 100',

                'cpi' =>
                    'total_spend / onboarding_start_active_users',

                'ctr_meta' =>
                    'meta_clicks / meta_impressions × 100',

                'ctr_google' =>
                    'google_clicks / google_impressions × 100',
            ],

            /*
            |--------------------------------------------------------------------------
            | TOTAL
            |--------------------------------------------------------------------------
            */

            'total' => [
                'meta_active_campaigns' =>
                    $metaActiveCampaignsCount,

                'google_active_campaigns' =>
                    $googleActiveCampaignsCount,

                'active_campaigns_total' =>
                    $metaActiveCampaignsCount
                    + $googleActiveCampaignsCount,

                'meta_spend' =>
                    round(
                        $totalMetaSpend,
                        2
                    ),

                'google_spend' =>
                    round(
                        $totalGoogleSpend,
                        2
                    ),

                'total_spend' =>
                    round(
                        $totalCombinedSpend,
                        2
                    ),

                'meta_impressions' =>
                    $totalMetaImpressions,

                'google_impressions' =>
                    $totalGoogleImpressions,

                'total_paid_impressions' =>
                    $totalPaidImpressions,

                'meta_clicks' =>
                    $totalMetaClicks,

                'google_clicks' =>
                    $totalGoogleClicks,

                'ctr_meta' =>
                    $globalCtrMeta,

                'ctr_google' =>
                    $globalCtrGoogle,

                /*
                |--------------------------------------------------------------------------
                | INSTALLS
                |--------------------------------------------------------------------------
                */

                'appsflyer_installs_android' =>
                    $totalAppsFlyerInstallsAndroid,

                'appsflyer_installs_ios' =>
                    $totalAppsFlyerInstallsIos,

                'appsflyer_installs_total' =>
                    $totalAppsFlyerInstalls,

                'appsflyer_installs_unattributed_os' =>
                    $totalAppsFlyerInstallsUnattributedOs,

                /*
                |--------------------------------------------------------------------------
                | ONBOARDING START
                |--------------------------------------------------------------------------
                */

                'onboarding_start_active_users' =>
                    $totalOnboardingStartActiveUsers,

                'onboarding_start_active_users_android' =>
                    $totalOnboardingStartAndroid,

                'onboarding_start_active_users_ios' =>
                    $totalOnboardingStartIos,

                'onboarding_start_active_users_unattributed_os' =>
                    $totalOnboardingStartUnattributedOs,

                /*
                |--------------------------------------------------------------------------
                | ONBOARDING SUCCESS
                |--------------------------------------------------------------------------
                */

                'onboarding_success_active_users' =>
                    $totalOnboardingSuccessActiveUsers,

                'onboarding_success_active_users_android' =>
                    $totalOnboardingSuccessAndroid,

                'onboarding_success_active_users_ios' =>
                    $totalOnboardingSuccessIos,

                'onboarding_success_active_users_unattributed_os' =>
                    $totalOnboardingSuccessUnattributedOs,

                /*
                 * Compatibilité dashboard.
                 */
                'installations' =>
                    $totalInstallations,

                'comptes' =>
                    $totalComptes,

                'onboarding_conversion_rate' =>
                    $globalOnboardingConversionRate,

                'cpi' =>
                    $globalCpi,

                'currency' =>
                    'MAD',
            ],

            /*
            |--------------------------------------------------------------------------
            | DATA
            |--------------------------------------------------------------------------
            */

            'data' =>
                array_values(
                    $dailyData
                ),

            /*
            |--------------------------------------------------------------------------
            | DEBUG
            |--------------------------------------------------------------------------
            */

            'debug' => [
                'endpoint_cache_version' =>
                    'v6',

                'meta_rows_count' =>
                    $metaRowsCount,

                'google_rows_count' =>
                    $googleRowsCount,

                'appsflyer_rows_count' =>
                    $appsflyerRowsCount,

                'appsflyer_api_reports_count' =>
                    $appsflyerApiReportsCount,

                'appsflyer_cached_reports_count' =>
                    $appsflyerCachedReportsCount,

                'appsflyer_skipped_rows_count' =>
                    $appsflyerSkippedRowsCount,

                'appsflyer_missing_user_id_count' =>
                    $appsflyerMissingUserIdCount,

                'appsflyer_report_mode' =>
                    '31_day_chunks_streaming_cached_budgeted',

                'appsflyer_chunks_count' =>
                    count(
                        $appsflyerDateChunks
                    ),

                'appsflyer_unavailable_reports' =>
                    $appsflyerUnavailableReports,

                'appsflyer_warnings' =>
                    $appsflyerWarnings,

                'source_warnings' =>
                    $sourceWarnings,

                /*
                |--------------------------------------------------------------------------
                | SOURCE DATES REELLEMENT OBSERVEES
                |--------------------------------------------------------------------------
                */

                'source_dates' =>
                    $sourceDates,

                /*
                |--------------------------------------------------------------------------
                | IMPORTANT
                |--------------------------------------------------------------------------
                |
                | Aujourd'hui le rapport installs total est construit avec
                | Android + iOS.
                |
                | Impossible donc de découvrir un troisième ensemble
                | "OS non attribué" à partir de cette implémentation.
                |--------------------------------------------------------------------------
                */

                'appsflyer_installs_total_source' =>
                    'android_plus_ios',

                'can_detect_unattributed_install_os' =>
                    false,

                /*
                 * Même problème pour les totals onboarding :
                 * le total actuel est Android + iOS.
                 */
                'appsflyer_onboarding_total_source' =>
                    'android_plus_ios',

                'can_detect_unattributed_onboarding_os' =>
                    false,

                'php_memory_peak_mb' =>
                    round(
                        memory_get_peak_usage(true)
                        / 1024
                        / 1024,
                        2
                    ),

                'fx_used' =>
                    $fx,
            ],
        ];

        /*
        |--------------------------------------------------------------------------
        | PREVIOUS PERIOD - TOTAL UNIQUEMENT
        |--------------------------------------------------------------------------
        |
        | On réutilise exactement la même fonction afin que les règles de calcul
        | Meta Ads, Google Ads, AppsFlyer, CTR, CPI et onboarding soient strictement
        | identiques à celles de la période courante.
        |
        | IMPORTANT : on ne copie PAS previous.data.
        | La réponse expose uniquement previous.total.
        |--------------------------------------------------------------------------
        */

        if ($hasPreviousPeriod) {
            $previousRequest = clone $request;

            $previousInput = $request->all();

            unset(
                $previousInput['start_date'],
                $previousInput['end_date'],
                $previousInput['since'],
                $previousInput['until'],
                $previousInput['previous_since'],
                $previousInput['previous_until']
            );

            $previousInput['client_id'] = (int) $clientId;
            $previousInput['since'] = $previousSince;
            $previousInput['until'] = $previousUntil;

            $previousRequest->replace($previousInput);

            /*
             * Flag interne : le second passage ne doit pas lui-même
             * essayer de recalculer une période précédente.
             */
            $previousRequest->attributes->set(
                '_skip_previous',
                true
            );

            $previousResponse =
                $this->getAppsFlyerDailyFunnel(
                    $previousRequest
                );

            $previousPayload =
                $previousResponse->getData(true);

            if (
                !is_array($previousPayload)
                || !($previousPayload['success'] ?? false)
            ) {
                throw new \RuntimeException(
                    $previousPayload['error']
                        ?? 'Impossible de calculer la période précédente'
                );
            }

            $payload['previous'] = [
                'total' =>
                    $previousPayload['total']
                    ?? [],
            ];
        }

        /*
        |--------------------------------------------------------------------------
        | DEBUG CACHE
        |--------------------------------------------------------------------------
        */

        $payload[
            'debug'
        ][
            'endpoint_cache_hit'
        ] = false;

        $payload[
            'debug'
        ][
            'appsflyer_network_elapsed_seconds'
        ] =
            round(
                microtime(true)
                - $appsflyerStartedAt,
                3
            );

        /*
        |--------------------------------------------------------------------------
        | ENDPOINT CACHE
        |--------------------------------------------------------------------------
        */

        $todayBusiness =
            now(
                $businessTimezone
            )->startOfDay();

        $containsTodayForEndpoint =
            $startDate->lte(
                $todayBusiness
            )
            && $endDate->gte(
                $todayBusiness
            );

        \Illuminate\Support\Facades\Cache::put(
            $endpointCacheKey,
            $payload,
            $containsTodayForEndpoint
                ? now(
                    $businessTimezone
                )->addMinutes(15)
                : now(
                    $businessTimezone
                )->addDays(7)
        );

        return response()->json($payload);

    } catch (\Throwable $e) {
        /*
        |--------------------------------------------------------------------------
        | GLOBAL ERROR
        |--------------------------------------------------------------------------
        */

        \Log::error(
            'DAILY MEDIA AND APPSFLYER UNIQUE USERS ERROR',
            [
                'client_id' =>
                    $clientId
                    ?? null,

                'meta_account_id' =>
                    $metaAdAccountId
                    ?? null,

                'google_customer_id' =>
                    $googleCustomerId
                    ?? null,

                'start_date' =>
                    $since
                    ?? null,

                'end_date' =>
                    $until
                    ?? null,

                'timezone' =>
                    $businessTimezone
                    ?? null,

                'message' =>
                    $e->getMessage(),

                'file' =>
                    $e->getFile(),

                'line' =>
                    $e->getLine(),
            ]
        );

        return response()->json([
            'success' => false,
            'error' => $e->getMessage(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
        ], 500);
    }
}
