<?php

namespace App\Services\Octoparse;

use App\Models\Octoparse\OctoparseImportLog;
use App\Models\Octoparse\OctoparseRecord;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;

class OctoparseImportService
{
    /** @var OctoparseApiClient */
    private $api;

    public function __construct(OctoparseApiClient $api)
    {
        $this->api = $api;
    }

    /**
     * Run import for one task. Returns the persisted log row.
     */
    public function importTask(
        string $taskId,
        array $taskConfig,
        bool $dryRun = false,
        bool $triggeredManually = false
    ): OctoparseImportLog {
        echo "▶ 5 — Entering importTask for {$taskId}\n";
        // dd(['taskId' => $taskId, 'taskConfig' => $taskConfig, 'dryRun' => $dryRun, 'triggeredManually' => $triggeredManually]);

        $startedAt = Carbon::now();

        $log = OctoparseImportLog::create([
            'task_id' => $taskId,
            'status' => OctoparseImportLog::STATUS_RUNNING,
            'dry_run' => $dryRun,
            'triggered_manually' => $triggeredManually,
            'started_at' => $startedAt,
            'metadata' => [
                'label' => $taskConfig['label'] ?? null,
                'unique_key_field' => $taskConfig['unique_key_field'] ?? null,
            ],
        ]);

        echo "▶ 6 — Import log row created (id={$log->id})\n";
        // dd($log->toArray());

        Log::info("[Octoparse] Import started for task {$taskId}", [
            'dry_run' => $dryRun,
            'triggered_manually' => $triggeredManually,
        ]);

        $insertedIds = [];

        try {
            $useNotExported = (bool) config('octoparse.import.use_not_exported_endpoint');
            echo "▶ 7 — Branch decision: " . ($useNotExported ? 'importNotExported' : 'importAllData') . "\n";
            // dd(['useNotExported' => $useNotExported]);

            $stats = $useNotExported
                ? $this->importNotExported($taskId, $taskConfig, $dryRun, $insertedIds)
                : $this->importAllData($taskId, $taskConfig, $dryRun, $insertedIds);

            echo "▶ 13 — Stats for {$taskId}: fetched={$stats['fetched']} inserted={$stats['inserted']} updated={$stats['updated']} failed={$stats['failed']}\n";
            // dd($stats);

            $log->fill([
                'status' => OctoparseImportLog::STATUS_SUCCESS,
                'rows_fetched' => $stats['fetched'],
                'rows_inserted' => $stats['inserted'],
                'rows_updated' => $stats['updated'],
                'rows_failed' => $stats['failed'],
                'new_record_ids' => $insertedIds,
                'finished_at' => Carbon::now(),
            ]);

            if ($stats['failed'] > 0) {
                $log->status = OctoparseImportLog::STATUS_PARTIAL;
            }

            $log->save();

            Log::info("[Octoparse] Import finished for task {$taskId}", $stats);
        } catch (\Throwable $e) {
            $log->fill([
                'status' => OctoparseImportLog::STATUS_FAILED,
                'error_message' => $e->getMessage(),
                'new_record_ids' => $insertedIds,
                'finished_at' => Carbon::now(),
            ])->save();

            Log::error("[Octoparse] Import failed for task {$taskId}: " . $e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw $e;
        }

        return $log;
    }

    /**
     * Pull every page via /api/alldata/GetDataOfTaskByOffset.
     */
    private function importAllData(string $taskId, array $taskConfig, bool $dryRun, array &$insertedIds): array
    {
        $pageSize = (int) config('octoparse.import.page_size');
        $offset = 0;

        $stats = ['fetched' => 0, 'inserted' => 0, 'updated' => 0, 'failed' => 0];

        do {
            $page = $this->api->getDataByOffset($taskId, $offset, $pageSize);
            echo "▶ 8 — Page fetched (offset={$offset} returned " . count($page['dataList']) . " rows, restTotal={$page['restTotal']})\n";
            // dd($page);
            $rows = $page['dataList'];
            $stats['fetched'] += count($rows);

            $this->processRows($taskId, $taskConfig, $rows, $dryRun, $stats, $insertedIds);

            // Octoparse paginates by returning the next offset; advance even
            // when the API sends back the same offset (defensive).
            $newOffset = $page['offset'] > $offset ? $page['offset'] : $offset + count($rows);
            $hasMore = count($rows) > 0 && $page['restTotal'] > 0;
            $offset = $newOffset;
        } while ($hasMore);

        return $stats;
    }

    /**
     * Pull only rows not exported yet, then mark them exported.
     */
    private function importNotExported(string $taskId, array $taskConfig, bool $dryRun, array &$insertedIds): array
    {
        $pageSize = (int) config('octoparse.import.page_size');
        $stats = ['fetched' => 0, 'inserted' => 0, 'updated' => 0, 'failed' => 0];

        do {
            $page = $this->api->getNotExportedData($taskId, $pageSize);
            echo "▶ 8 — Page fetched (notExported returned " . count($page['dataList']) . " rows, total={$page['total']})\n";
            // dd($page);
            $rows = $page['dataList'];
            $stats['fetched'] += count($rows);

            $this->processRows($taskId, $taskConfig, $rows, $dryRun, $stats, $insertedIds);

            if (!$dryRun && count($rows) > 0) {
                echo "▶ 16 — Marking data exported for task {$taskId} (rows seen=" . count($rows) . ")\n";
                // dd(['taskId' => $taskId, 'rows_count' => count($rows), 'dryRun' => $dryRun]);
                $this->api->markDataExported($taskId);
            }

            $hasMore = count($rows) >= $pageSize;
        } while ($hasMore);

        return $stats;
    }

    private function processRows(
        string $taskId,
        array $taskConfig,
        array $rows,
        bool $dryRun,
        array &$stats,
        array &$insertedIds
    ): void {
        $importedAt = Carbon::now();
        $uniqueField = $taskConfig['unique_key_field'] ?? null;

        foreach ($rows as $row) {
            try {
                $externalKey = $this->resolveExternalKey($row, $uniqueField);
                $hash = OctoparseRecord::hashKey($externalKey);

                $attributes = ['task_id' => $taskId, 'external_key_hash' => $hash];
                $values = [
                    'external_key' => mb_substr($externalKey, 0, 65000),
                    'payload' => $row,
                    'scraped_at' => $this->resolveScrapedAt($row),
                    'imported_at' => $importedAt,
                ];

                echo "▶ 11 — Row resolved key={$externalKey} hash=" . substr($hash, 0, 12) . "...\n";
                // dd(['row' => $row, 'externalKey' => $externalKey, 'hash' => $hash, 'attributes' => $attributes, 'values' => $values]);

                echo "▶ 12 — Upsert path: " . ($dryRun ? 'DRY-RUN (no write)' : 'REAL upsert') . "\n";
                // dd(['dryRun' => $dryRun, 'existing' => optional(OctoparseRecord::where($attributes)->first())->toArray()]);

                if ($dryRun) {
                    $exists = OctoparseRecord::where($attributes)->exists();
                    $exists ? $stats['updated']++ : $stats['inserted']++;
                    continue;
                }

                $existing = OctoparseRecord::where($attributes)->first();
                if ($existing) {
                    $existing->fill($values)->save();
                    $stats['updated']++;
                } else {
                    $created = OctoparseRecord::create(array_merge($attributes, $values));
                    $insertedIds[] = $created->id;
                    $stats['inserted']++;
                }
            } catch (\Throwable $e) {
                $stats['failed']++;
                Log::warning("[Octoparse] Row import failed for task {$taskId}: " . $e->getMessage(), [
                    'row' => $row,
                ]);
            }
        }
    }

    /**
     * Resolve a stable upsert key for a row. Falls back to a sha256 of the
     * whole payload when no unique field is configured.
     */
    private function resolveExternalKey(array $row, ?string $uniqueField): string
    {
        if (!empty($uniqueField) && array_key_exists($uniqueField, $row)) {
            $value = $row[$uniqueField];
            if (is_array($value) || is_object($value)) {
                $value = json_encode($value);
            }
            $value = trim((string) $value);
            if ($value !== '') {
                return $value;
            }
        }

        // Fallback: hash of full payload — every row stays unique, so it acts
        // as append-only when no key is configured.
        return 'payload:' . hash('sha256', json_encode($row));
    }

    private function resolveScrapedAt(array $row): ?Carbon
    {
        foreach (['ScrapedTime', 'scraped_at', 'collection_time'] as $candidate) {
            if (!empty($row[$candidate])) {
                try {
                    return Carbon::parse($row[$candidate]);
                } catch (\Throwable $e) {
                    // ignore and try the next candidate
                }
            }
        }
        return null;
    }
}
