<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Tools;

use PrettyLinks\Pro\Reports\CustomReportData;

/**
 * CSV export for a single Custom Report — one row per source link, with
 * clicks / uniques (and conversions / conversion rate when the report has
 * a goal link). Column headers mirror the on-screen table so the file
 * reads like the report the admin sees.
 *
 * Built on `AggregateCsvExporter`: the row set comes from
 * `CustomReportData::build()` (the same composition the JSON view uses),
 * and the base handles chunk stitching + injection escaping so the shared
 * `chunkedDownloadCsv` client drives the download unchanged.
 *
 * Expected `$args`: `report_id` (int), `from` (string), `to` (string).
 */
class CustomReportCsvExporter extends AggregateCsvExporter
{
    /**
     * Report data composer.
     *
     * @var CustomReportData
     */
    private CustomReportData $data;

    /**
     * Memoized built payload keyed by the report_id/from/to signature so
     * `columns()` (needs goal presence) and `computeRows()` share one build.
     *
     * @var array<string, mixed>|null
     */
    private ?array $built = null;

    /**
     * Cache key for the memoized build.
     *
     * @var string
     */
    private string $builtKey = '';

    /**
     * Construct the exporter with its report-data composer.
     *
     * @param CustomReportData $data Report data composer.
     */
    public function __construct(CustomReportData $data)
    {
        $this->data = $data;
    }

    /**
     * Column headers. Conversions columns are added only when the report
     * has a live goal link (mirrors the on-screen table).
     *
     * @param  array<string, mixed> $args Export arguments.
     * @return string[]
     */
    protected function columns(array $args): array
    {
        return array_values($this->labels($this->hasGoal($args)));
    }

    /**
     * One row per source link, sorted to match the table's default view
     * (most clicks first, then name).
     *
     * @param  array<string, mixed> $args Export arguments.
     * @return array<int, array<string, mixed>>
     */
    protected function computeRows(array $args): array
    {
        $built = $this->report($args);
        if ($built === null) {
            return [];
        }

        $hasGoal = $this->hasGoal($args);
        $labels  = $this->labels($hasGoal);
        $sources = is_array($built['sources']) ? $built['sources'] : [];

        usort($sources, static function (array $a, array $b): int {
            $byClicks = ((int) ($b['clicks'] ?? 0)) <=> ((int) ($a['clicks'] ?? 0));
            if ($byClicks !== 0) {
                return $byClicks;
            }
            return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
        });

        $rows = [];
        foreach ($sources as $source) {
            $row = [
                $labels['name']    => (string) ($source['name'] ?? ''),
                $labels['slug']    => (string) ($source['slug'] ?? ''),
                $labels['url']     => (string) ($source['url'] ?? ''),
                $labels['clicks']  => (string) (int) ($source['clicks'] ?? 0),
                $labels['uniques'] => (string) (int) ($source['uniques'] ?? 0),
            ];
            if ($hasGoal) {
                $row[$labels['conversions']] = (string) (int) ($source['conversions'] ?? 0);
                $row[$labels['convRate']]    = number_format((float) ($source['convRate'] ?? 0), 2, '.', '');
            }
            $rows[] = $row;
        }
        return $rows;
    }

    /**
     * Semantic-key → translated header label map. Single source used by
     * both `columns()` and `computeRows()` so row keys always match the
     * header row.
     *
     * @param  boolean $hasGoal Whether to include the conversion columns.
     * @return array<string, string>
     */
    private function labels(bool $hasGoal): array
    {
        $labels = [
            'name'    => __('Source', 'pretty-link-pro'),
            'slug'    => __('Slug', 'pretty-link-pro'),
            'url'     => __('Target URL', 'pretty-link-pro'),
            'clicks'  => __('All Clicks', 'pretty-link-pro'),
            'uniques' => __('Unique Clicks', 'pretty-link-pro'),
        ];
        if ($hasGoal) {
            $labels['conversions'] = __('Conversions', 'pretty-link-pro');
            $labels['convRate']    = __('Conversion Rate (%)', 'pretty-link-pro');
        }
        return $labels;
    }

    /**
     * Whether the built report has a live goal link.
     *
     * @param array<string, mixed> $args Export arguments.
     */
    private function hasGoal(array $args): bool
    {
        $built = $this->report($args);
        return $built !== null && $built['goalLinkId'] !== null && (int) $built['goalLinkId'] > 0;
    }

    /**
     * Memoized report build for the given args.
     *
     * @param  array<string, mixed> $args Export arguments.
     * @return array<string, mixed>|null
     */
    private function report(array $args): ?array
    {
        $reportId = (int) ($args['report_id'] ?? 0);
        $from     = (string) ($args['from'] ?? '');
        $to       = (string) ($args['to'] ?? '');
        $key      = md5($reportId . '|' . $from . '|' . $to);
        if ($this->builtKey !== $key) {
            $this->built    = $this->data->build($reportId, $from, $to);
            $this->builtKey = $key;
        }
        return $this->built;
    }
}
