<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Rest;

// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter
// Custom plugin tables (prli_*): table names interpolated from $wpdb->prefix (trusted),
// user values bind through $wpdb->prepare(). No caching: these tables are the source
// of truth for click/redirect data and must read-through. "meta_key"/"meta_value" here
// refer to our own prli_link_metas table, not wp_postmeta.
use PrettyLinks\Pro\Reports\CustomReportData;
use PrettyLinks\Pro\Repositories\Reports as ReportsRepo;
use PrettyLinks\Pro\Tools\AggregateCsvExporter;
use PrettyLinks\Pro\Tools\CustomReportCsvExporter;
use PrettyLinks\Repositories\Links as LinksRepo;
use PrettyLinks\Rest\Controllers\BaseController;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Pro Custom Reports REST endpoints at `/pro/reports`.
 *
 * CRUD + the composed `{id}/data` payload that powers the report view.
 * Namespace is the shared `pretty-links/v1`; the `/pro/` path prefix is
 * the convention used for every Pro-only endpoint (see
 * `SplitTestController`).
 *
 * CRUD endpoints talk to `Repositories\Reports`; the `{id}/data` endpoint
 * composes `Repositories\Reports::find()` + `CustomReports::summaryBySource()`
 * + (when a goal is configured) `Conversions::bySource()` and normalizes
 * the payload shape consumed by the React app.
 */
class CustomReportsController extends BaseController
{
    /**
     * Register the custom reports REST routes.
     *
     * @return void
     */
    public function register(): void
    {
        register_rest_route($this->namespace(), '/pro/reports', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [$this, 'index'],
                'permission_callback' => $this->permission(),
                'args'                => [
                    'orderby' => [
                        'type'    => 'string',
                        'default' => 'created_at',
                    ],
                    'order'   => [
                        'type'    => 'string',
                        'enum'    => ['asc', 'desc'],
                        'default' => 'desc',
                    ],
                ],
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [$this, 'create'],
                'permission_callback' => $this->permission(),
            ],
        ]);

        register_rest_route($this->namespace(), '/pro/reports/(?P<id>\d+)', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [$this, 'show'],
                'permission_callback' => $this->permission(),
            ],
            [
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => [$this, 'update'],
                'permission_callback' => $this->permission(),
            ],
            [
                'methods'             => WP_REST_Server::DELETABLE,
                'callback'            => [$this, 'delete'],
                'permission_callback' => $this->permission(),
            ],
        ]);

        register_rest_route($this->namespace(), '/pro/reports/(?P<id>\d+)/data', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [$this, 'data'],
                'permission_callback' => $this->permission(),
                'args'                => [
                    'from' => ['type' => 'string'],
                    'to'   => ['type' => 'string'],
                ],
            ],
        ]);

        register_rest_route($this->namespace(), '/pro/reports/(?P<id>\d+)/export', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [$this, 'export'],
                'permission_callback' => $this->permission(),
                'args'                => [
                    'from'       => ['type' => 'string'],
                    'to'         => ['type' => 'string'],
                    'offset'     => ['type' => 'integer'],
                    'limit'      => ['type' => 'integer'],
                    'count_only' => ['type' => 'boolean'],
                ],
            ],
        ]);
    }

    /**
     * Return the list of custom reports.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return WP_REST_Response
     */
    public function index(WP_REST_Request $request): WP_REST_Response
    {
        $orderby = (string) $request->get_param('orderby');
        $order   = (string) $request->get_param('order');

        $repo = new ReportsRepo();
        $rows = $repo->all($orderby, $order);

        // Batch-fetch goal link names so the list view can render them
        // without one query per row.
        $goalIds = [];
        foreach ($rows as $row) {
            if ($row['goal_link_id'] !== null && $row['goal_link_id'] > 0) {
                $goalIds[$row['goal_link_id']] = true;
            }
        }
        $goalNames = $this->batchLinkNames(array_keys($goalIds));

        $out = [];
        foreach ($rows as $row) {
            $out[] = [
                'id'           => $row['id'],
                'name'         => $row['name'],
                'goalLinkId'   => $row['goal_link_id'],
                'goalLinkName' => $row['goal_link_id'] !== null && isset($goalNames[$row['goal_link_id']])
                    ? $goalNames[$row['goal_link_id']]
                    : '',
                'createdAt'    => $row['created_at'],
                'linkCount'    => $row['link_count'],
            ];
        }
        return new WP_REST_Response($out);
    }

    /**
     * Create a custom report.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function create(WP_REST_Request $request)
    {
        $body = $this->validateBody($request);
        if ($body instanceof WP_Error) {
            return $body;
        }

        $repo = new ReportsRepo();
        $id   = $repo->create($body);
        if ($id <= 0) {
            return new WP_Error('insert_failed', __('Unable to create report.', 'pretty-link-pro'), ['status' => 500]);
        }

        $report = $repo->find($id);
        return new WP_REST_Response($this->serializeReport($report), 201);
    }

    /**
     * Return a single custom report by id.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function show(WP_REST_Request $request)
    {
        $id     = (int) $request['id'];
        $report = (new ReportsRepo())->find($id);
        if ($report === null) {
            return new WP_Error('not_found', __('Report not found.', 'pretty-link-pro'), ['status' => 404]);
        }
        return new WP_REST_Response($this->serializeReport($report));
    }

    /**
     * Update a custom report by id.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function update(WP_REST_Request $request)
    {
        $id   = (int) $request['id'];
        $body = $this->validateBody($request);
        if ($body instanceof WP_Error) {
            return $body;
        }

        $repo = new ReportsRepo();
        $ok   = $repo->update($id, $body);
        if (!$ok) {
            $existing = $repo->find($id);
            if ($existing === null) {
                return new WP_Error('not_found', __('Report not found.', 'pretty-link-pro'), ['status' => 404]);
            }
            return new WP_Error('update_failed', __('Unable to update report.', 'pretty-link-pro'), ['status' => 500]);
        }

        $report = $repo->find($id);
        return new WP_REST_Response($this->serializeReport($report));
    }

    /**
     * Delete a custom report by id.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function delete(WP_REST_Request $request)
    {
        $id   = (int) $request['id'];
        $repo = new ReportsRepo();
        if ($repo->find($id) === null) {
            return new WP_Error('not_found', __('Report not found.', 'pretty-link-pro'), ['status' => 404]);
        }
        if (!$repo->delete($id)) {
            return new WP_Error('delete_failed', __('Unable to delete report.', 'pretty-link-pro'), ['status' => 500]);
        }
        return new WP_REST_Response([
            'deleted' => true,
            'id'      => $id,
        ]);
    }

    /**
     * Composed report data: metadata + per-source analytics +
     * conversions when a goal link is configured.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function data(WP_REST_Request $request)
    {
        $id      = (int) $request['id'];
        $payload = $this->reportData()->build(
            $id,
            (string) $request->get_param('from'),
            (string) $request->get_param('to')
        );
        if ($payload === null) {
            return new WP_Error('not_found', __('Report not found.', 'pretty-link-pro'), ['status' => 404]);
        }

        return new WP_REST_Response($payload);
    }

    /**
     * Export a report's per-source data as CSV.
     *
     * Honors the shared chunk contract consumed by
     * `src-js/shared/csv-download.js`: `count_only` returns a row count,
     * `offset`/`limit` return a chunk, otherwise a single-shot `csv`.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function export(WP_REST_Request $request)
    {
        $id = (int) $request['id'];
        if ((new ReportsRepo())->find($id) === null) {
            return new WP_Error('not_found', __('Report not found.', 'pretty-link-pro'), ['status' => 404]);
        }

        $args = [
            'report_id' => $id,
            'from'      => (string) $request->get_param('from'),
            'to'        => (string) $request->get_param('to'),
        ];

        $exporter = new CustomReportCsvExporter($this->reportData());

        if ($request->get_param('count_only')) {
            return new WP_REST_Response(['total' => $exporter->total($args)]);
        }

        $offsetParam = $request->get_param('offset');
        if ($offsetParam !== null && $offsetParam !== '') {
            $offset = (int) $offsetParam;
            $limit  = (int) ($request->get_param('limit') ?: AggregateCsvExporter::DEFAULT_CHUNK_SIZE);
            return new WP_REST_Response($exporter->chunk($args, $offset, $limit));
        }

        return new WP_REST_Response(['csv' => $exporter->fullCsv($args)]);
    }

    /**
     * Build the report-data composer, wired with the links repository.
     *
     * @return CustomReportData
     */
    private function reportData(): CustomReportData
    {
        return new CustomReportData($this->linksRepo());
    }

    /**
     * Validate and normalize a create/update request body.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return array<string, mixed>|WP_Error
     */
    private function validateBody(WP_REST_Request $request)
    {
        $body = $request->get_json_params();
        if (!is_array($body)) {
            $body = $request->get_body_params();
        }
        if (!is_array($body)) {
            $body = [];
        }

        $name = isset($body['name']) ? trim((string) $body['name']) : '';
        if ($name === '') {
            return new WP_Error(
                'invalid_name',
                __('Report name is required.', 'pretty-link-pro'),
                ['status' => 400]
            );
        }

        $rawLinkIds = $body['link_ids'] ?? ($body['linkIds'] ?? []);
        if (!is_array($rawLinkIds)) {
            return new WP_Error(
                'invalid_link_ids',
                __('At least one source link is required.', 'pretty-link-pro'),
                ['status' => 400]
            );
        }
        $linkIds = [];
        foreach ($rawLinkIds as $value) {
            $id = (int) $value;
            if ($id > 0) {
                $linkIds[] = $id;
            }
        }
        if ($linkIds === []) {
            return new WP_Error(
                'invalid_link_ids',
                __('At least one source link is required.', 'pretty-link-pro'),
                ['status' => 400]
            );
        }

        $goalLinkId = isset($body['goal_link_id'])
            ? (int) $body['goal_link_id']
            : (isset($body['goalLinkId']) ? (int) $body['goalLinkId'] : 0);

        return [
            'name'         => $name,
            'link_ids'     => $linkIds,
            'goal_link_id' => $goalLinkId > 0 ? $goalLinkId : 0,
        ];
    }

    /**
     * Resolve the links repository from the container.
     *
     * @return LinksRepo
     */
    private function linksRepo(): LinksRepo
    {
        return $this->container->get(LinksRepo::class);
    }

    /**
     * Shape a report row into its REST representation.
     *
     * @param  array{id: int, name: string, goal_link_id: int|null, created_at: string, link_ids: array<int, int>}|null $report Report row to serialize.
     * @return array<string, mixed>
     */
    private function serializeReport(?array $report): array
    {
        if ($report === null) {
            return [];
        }
        $goalId   = $report['goal_link_id'];
        $goalName = '';
        if ($goalId !== null && $goalId > 0) {
            $names    = $this->batchLinkNames([$goalId]);
            $goalName = $names[$goalId] ?? '';
        }
        return [
            'id'           => $report['id'],
            'name'         => $report['name'],
            'goalLinkId'   => $goalId,
            'goalLinkName' => $goalName,
            'createdAt'    => $report['created_at'],
            'linkIds'      => $report['link_ids'],
        ];
    }

    /**
     * Fetch names for a batch of link ids in a single query. Returns a
     * map keyed by link id. Skips deleted links (their `name` still
     * resolves — we only care about existence).
     *
     * @param  array<int, int> $ids Link ids to fetch names for.
     * @return array<int, string>
     */
    private function batchLinkNames(array $ids): array
    {
        $ids = array_values(array_filter(array_map('intval', $ids), static function (int $id): bool {
            return $id > 0;
        }));
        if ($ids === []) {
            return [];
        }
        global $wpdb;
        $table        = $wpdb->prefix . 'prli_links';
        $placeholders = implode(',', array_fill(0, count($ids), '%d'));

        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $rows = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare("SELECT id, name FROM {$table} WHERE id IN ({$placeholders})", $ids),
            ARRAY_A
        ) ?: [];

        $out = [];
        foreach ($rows as $row) {
            $out[(int) $row['id']] = (string) $row['name'];
        }
        return $out;
    }
}
