<?php

namespace App\Services;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class NotionService
{
    private const BASE_URL       = 'https://api.notion.com/v1';
    private const NOTION_VERSION = '2022-06-28';

    private PendingRequest $http;

    public function __construct()
    {
        $this->http = Http::withHeaders([
            'Authorization'  => 'Bearer ' . config('services.notion.api_key'),
            'Notion-Version' => self::NOTION_VERSION,
            'Content-Type'   => 'application/json',
        ]);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Page Operations
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Retrieve a page's properties from Notion.
     * Returns the full page object including all property values.
     */
    public function getPage(string $pageId): array
    {
        $response = $this->http->get(self::BASE_URL . "/pages/{$pageId}");

        if ($response->failed()) {
            Log::error('NotionService::getPage failed', [
                'page_id' => $pageId,
                'status'  => $response->status(),
                'body'    => $response->body(),
            ]);
            throw new \RuntimeException("Failed to fetch Notion page {$pageId}: " . $response->body());
        }

        return $response->json();
    }

    /**
     * Retrieve the block children of a page and return as plain text.
     * Iterates through paragraphs, headings, bulleted lists, code blocks.
     */
    public function getPageContent(string $pageId): string
    {
        $response = $this->http->get(self::BASE_URL . "/blocks/{$pageId}/children");

        if ($response->failed()) {
            Log::warning('NotionService::getPageContent failed', [
                'page_id' => $pageId,
                'status'  => $response->status(),
            ]);
            return '';
        }

        $blocks = $response->json('results', []);
        $text   = [];

        foreach ($blocks as $block) {
            $type    = $block['type'] ?? '';
            $content = $block[$type]['rich_text'] ?? [];

            $plain = implode('', array_map(
                fn($rt) => $rt['plain_text'] ?? '',
                $content
            ));

            if (! empty($plain)) {
                $text[] = match ($type) {
                    'heading_1'           => "# {$plain}",
                    'heading_2'           => "## {$plain}",
                    'heading_3'           => "### {$plain}",
                    'bulleted_list_item'  => "- {$plain}",
                    'numbered_list_item'  => "1. {$plain}",
                    'code'                => "```\n{$plain}\n```",
                    default               => $plain,
                };
            }
        }

        return implode("\n", $text);
    }

    /**
     * Create a new child page under a parent page.
     *
     * @param  string  $parentId  The ID of the parent page.
     * @param  string  $title     The new page's title.
     * @param  array   $content   Array of Notion block objects.
     * @return array              Created page object from Notion.
     */
    public function createPage(string $parentId, string $title, array $content = []): array
    {
        $payload = [
            'parent' => [
                'type'    => 'page_id',
                'page_id' => $parentId,
            ],
            'properties' => [
                'title' => [
                    'title' => [
                        ['type' => 'text', 'text' => ['content' => $title]],
                    ],
                ],
            ],
            'children' => $content,
        ];

        $response = $this->http->post(self::BASE_URL . '/pages', $payload);

        if ($response->failed()) {
            Log::error('NotionService::createPage failed', [
                'parent_id' => $parentId,
                'title'     => $title,
                'status'    => $response->status(),
                'body'      => $response->body(),
            ]);
            throw new \RuntimeException("Failed to create Notion page '{$title}': " . $response->body());
        }

        Log::info('NotionService::createPage success', [
            'title'   => $title,
            'page_id' => $response->json('id'),
        ]);

        return $response->json();
    }

    /**
     * Update a single property on an existing Notion page.
     *
     * @param  string  $pageId    The page to update.
     * @param  string  $property  The property name as shown in Notion.
     * @param  mixed   $value     The new property value (Notion property object).
     */
    public function updatePageProperty(string $pageId, string $property, mixed $value): void
    {
        $response = $this->http->patch(self::BASE_URL . "/pages/{$pageId}", [
            'properties' => [$property => $value],
        ]);

        if ($response->failed()) {
            Log::error('NotionService::updatePageProperty failed', [
                'page_id'  => $pageId,
                'property' => $property,
                'status'   => $response->status(),
                'body'     => $response->body(),
            ]);
            throw new \RuntimeException("Failed to update property '{$property}' on page {$pageId}");
        }
    }

    /**
     * Query a Notion database with optional filter. Follows pagination.
     */
    public function queryDatabase(string $databaseId, array $filter = [], array $sorts = []): array
    {
        $results = [];
        $cursor  = null;

        do {
            $payload = array_filter([
                'filter'       => $filter ?: null,
                'sorts'        => $sorts ?: null,
                'start_cursor' => $cursor,
            ]);

            $response = $this->http->post(self::BASE_URL . "/databases/{$databaseId}/query", $payload);

            if ($response->failed()) {
                throw new \RuntimeException("Failed to query Notion database {$databaseId}: " . $response->body());
            }

            $results = array_merge($results, $response->json('results', []));
            $cursor  = $response->json('has_more') ? $response->json('next_cursor') : null;
        } while ($cursor !== null);

        return $results;
    }

    /**
     * Fetch all documented tasks, optionally narrowed to a sprint.
     *
     * $statuses is the human review gate: "Generated" docs are drafts
     * (dev area only); the public site must only receive "Published".
     */
    public function queryDocumentedTasks(?string $sprintPageId = null, array $statuses = ['Generated', 'Published']): array
    {
        $statusFilter = count($statuses) === 1
            ? ['property' => 'Documentation Status', 'select' => ['equals' => $statuses[0]]]
            : ['or' => array_map(
                fn (string $s) => ['property' => 'Documentation Status', 'select' => ['equals' => $s]],
                $statuses
            )];

        $conditions = [$statusFilter];

        if ($sprintPageId) {
            $conditions[] = ['property' => 'Sprint', 'relation' => ['contains' => $sprintPageId]];
        }

        return $this->queryDatabase(
            config('services.notion.tasks_database_id'),
            count($conditions) > 1 ? ['and' => $conditions] : $conditions[0],
            [['timestamp' => 'last_edited_time', 'direction' => 'descending']]
        );
    }

    /**
     * Resolve a sprint page in the Sprints database by (partial) title match.
     * Returns the sprint page ID or null if not found.
     */
    public function findSprintByLabel(string $label): ?string
    {
        $sprintsDb = config('services.notion.sprints_database_id');

        if (! $sprintsDb) {
            return null;
        }

        // Title property name varies per workspace — match in PHP instead.
        foreach ($this->queryDatabase($sprintsDb) as $sprint) {
            foreach ($sprint['properties'] ?? [] as $prop) {
                if (($prop['type'] ?? '') !== 'title') {
                    continue;
                }
                $title = implode('', array_map(fn ($rt) => $rt['plain_text'] ?? '', $prop['title'] ?? []));
                if ($title !== '' && stripos($title, $label) !== false) {
                    return $sprint['id'];
                }
            }
        }

        return null;
    }

    /**
     * Build the release-note list for a sprint: every documented task,
     * with its user-facing note body pulled from the generated Release Note page.
     *
     * @return array<int, array{title: string, category: string, note: string}>
     */
    public function releaseNotesForSprint(?string $sprintLabel = null): array
    {
        $sprintPageId = null;

        if ($sprintLabel) {
            $sprintPageId = $this->findSprintByLabel($sprintLabel);

            if (! $sprintPageId) {
                Log::warning('NotionService: sprint label not found, using all documented tasks', [
                    'sprint' => $sprintLabel,
                ]);
            }
        }

        return array_map(function (array $task) {
            $notePageId = \App\Support\NotionTask::pageIdFromUrl(
                \App\Support\NotionTask::releaseNoteUrl($task)
            );

            return [
                'title'    => \App\Support\NotionTask::cleanTitle($task),
                'category' => \App\Support\NotionTask::category($task),
                'note'     => $notePageId
                    ? $this->getPageContent($notePageId)
                    : \App\Support\NotionTask::conclusion($task),
            ];
        }, $this->queryDocumentedTasks($sprintPageId));
    }

    /**
     * Map of sprint page ID => title, for the Pátio do Porto (Vessel = Sprint).
     */
    public function sprintTitles(): array
    {
        $sprintsDb = config('services.notion.sprints_database_id');

        if (! $sprintsDb) {
            return [];
        }

        $titles = [];

        foreach ($this->queryDatabase($sprintsDb) as $sprint) {
            $title = \App\Support\NotionTask::pageTitle($sprint);

            if ($title !== '') {
                $titles[$sprint['id']] = $title;
            }
        }

        return $titles;
    }

    /**
     * All tasks (documented or not) linked to a sprint — used to compute
     * "documented vs pending" totals for the vessel view.
     */
    public function queryTasksBySprint(string $sprintPageId): array
    {
        return $this->queryDatabase(
            config('services.notion.tasks_database_id'),
            ['property' => 'Sprint', 'relation' => ['contains' => $sprintPageId]]
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Block Operations
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Retrieve all block children of a page or block, following pagination.
     * Returns raw Notion block objects (for rich HTML rendering).
     */
    public function getBlocks(string $blockId): array
    {
        $blocks = [];
        $cursor = null;

        do {
            $query = array_filter(['start_cursor' => $cursor, 'page_size' => 100]);

            $response = $this->http->get(self::BASE_URL . "/blocks/{$blockId}/children", $query);

            if ($response->failed()) {
                Log::warning('NotionService::getBlocks failed', [
                    'block_id' => $blockId,
                    'status'   => $response->status(),
                ]);
                return $blocks;
            }

            $blocks = array_merge($blocks, $response->json('results', []));
            $cursor = $response->json('has_more') ? $response->json('next_cursor') : null;
        } while ($cursor !== null);

        return $blocks;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Database Schema
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Add or update properties on a database schema.
     * Used by `manifest:setup` to add the documentation tracking properties.
     */
    public function updateDatabaseSchema(string $databaseId, array $properties): array
    {
        $response = $this->http->patch(self::BASE_URL . "/databases/{$databaseId}", [
            'properties' => $properties,
        ]);

        if ($response->failed()) {
            throw new \RuntimeException("Failed to update database schema {$databaseId}: " . $response->body());
        }

        return $response->json();
    }
}