<?php

namespace WP_Statistics\Service\Database\Operations;

use RuntimeException;
use WP_STATISTICS\Option;

/**
 * Handles data insertion and migration between database tables.
 *
 * This class provides methods to:
 * - Migrate data from a source table to a target table.
 * - Directly insert data into a target table.
 * - Update existing records based on `conditions` (e.g., `visitor_id`).
 */
class Insert extends AbstractTableOperation
{
    /**
     * The source table for data migration.
     *
     * @var string
     */
    protected $sourceTable;

    /**
     * The source table name (with WordPress prefix).
     *
     * @var string
     */
    protected $prefixedSourceTable;

    /**
     * Set the source table (old table) for the operation.
     *
     * @param string $sourceTable
     * @return $this
     */
    public function setSourceTable(string $sourceTable)
    {
        $this->sourceTable         = $sourceTable;
        $this->prefixedSourceTable = $this->wpdb->prefix . 'statistics_' . $sourceTable;

        return $this;
    }

    /**
     * Execute the insert operation, supporting migration, inserts, and updates.
     *
     * @return void
     * @throws RuntimeException
     */
    public function execute()
    {
        try {
            $this->ensureConnection();
            $this->validateTableName();
            $this->validateArgs();
            $this->setFullTableName();

            if (!empty($this->sourceTable)) {
                $this->transactionHandler->executeInTransaction([$this, 'migrateData']);
            } elseif (!empty($this->args['conditions'])) {
                $this->transactionHandler->executeInTransaction([$this, 'insertOrUpdateData']);
            } else {
                $this->transactionHandler->executeInTransaction([$this, 'insertData']);
            }
        } catch (\Exception $e) {
            Option::saveOptionGroup('migration_status_detail', [
                'status' => 'failed',
                'message' => $e->getMessage()
            ], 'db');

            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, message is not rendered to HTML
            throw new RuntimeException(
                sprintf("Failed to insert/update data in table `%s`: %s", $this->tableName, $e->getMessage())
            );
        }
    }

    /**
     * Handles inserting or updating data based on `conditions` (e.g., visitor_id).
     */
    public function insertOrUpdateData()
    {
        $mapping = $this->args['mapping'] ?? [];
        $conditions = $this->args['conditions'] ?? [];

        if (empty($mapping) || empty($conditions)) {
            throw new RuntimeException("Mapping and conditions are required for updating data.");
        }

        // Construct WHERE clause from conditions
        $whereClauses = [];
        $params = [];

        foreach ($conditions as $column => $value) {
            $whereClauses[] = "`$column` = %s";
            $params[] = $value;
        }

        $whereQuery = implode(' AND ', $whereClauses);

        // Check if a matching record exists
        $existsQuery = "SELECT COUNT(*) FROM {$this->fullName} WHERE $whereQuery";
        $exists = $this->wpdb->get_var($this->wpdb->prepare($existsQuery, ...$params));

        if ($exists > 0) {
            // Update existing record
            $result = $this->wpdb->update($this->fullName, $mapping, $conditions);
            if ($result === false) {
                // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, message is not rendered to HTML
                throw new RuntimeException("Failed to update data: {$this->wpdb->last_error}");
            }
        } else {
            // Insert new record
            $mergedData = array_merge($mapping, $conditions);
            $result = $this->wpdb->insert($this->fullName, $mergedData);
            if ($result === false) {
                // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, message is not rendered to HTML
                throw new RuntimeException("Failed to insert data: {$this->wpdb->last_error}");
            }
        }
    }

    /**
     * Migrate data operation to be executed within a transaction.
     *
     * @return void
     * @throws RuntimeException
     */
    public function migrateData()
    {
        if (empty($this->sourceTable)) {
            throw new RuntimeException("Source table is not specified for migration.");
        }

        $mapping = $this->args['mapping'] ?? [];
        $distinctFields = $this->args['distinct_fields'] ?? [];
        $sourceTableSet = $this->args['source_table_set'] ?? [];

        if (empty($mapping)) {
            throw new RuntimeException("Mapping is required for migration.");
        }

        $batchSize = $this->args['batch_size'] ?? 50;
        $offset = $this->args['offset'] ?? 0;

        // Prepare the columns for fetching data from the source table
        $sourceColumns = implode(', ', array_values($mapping));

        $rows = $this->wpdb->get_results(
            $this->wpdb->prepare(
                "SELECT $sourceColumns FROM {$this->prefixedSourceTable} LIMIT %d OFFSET %d",
                $batchSize,
                $offset
            ),
            ARRAY_A
        );

        if ($rows === null || $rows === false) {
            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, message is not rendered to HTML
            throw new RuntimeException("Failed to fetch rows: {$this->wpdb->last_error}");
        }

        foreach ($rows as $row) {
            $mappedRow = [];
            foreach ($mapping as $targetColumn => $sourceColumn) {
                if (isset($row[$sourceColumn])) {
                    $mappedRow[$targetColumn] = $row[$sourceColumn];
                }
            }

            if ($this->shouldSkipDuplicate($mappedRow, $distinctFields)) {
                continue;
            }

            $this->performInsert($mappedRow);
        }
    }

    /**
     * Determines whether a row should be skipped based on `distinct_fields`.
     *
     * @param array $row
     * @param array $distinctFields
     * @return bool
     */
    private function shouldSkipDuplicate(array $row, array $distinctFields): bool
    {
        if (empty($distinctFields)) {
            return false;
        }

        $conditions = [];
        foreach ($distinctFields as $field) {
            if (isset($row[$field])) {
                $conditions[] = $this->wpdb->prepare("`$field` = %s", $row[$field]);
            }
        }

        if (empty($conditions)) {
            return false;
        }

        $conditionQuery = implode(' AND ', $conditions);
        $exists = $this->wpdb->get_var("SELECT COUNT(*) FROM {$this->fullName} WHERE $conditionQuery");

        return $exists > 0;
    }
}
