<?php
/**
 * Plugin Changelog Service - Reads changelog from plugin readme/changelog files.
 *
 * @package FleekDash\Controllers\Plugins
 * @since 2.2.0
 */

namespace FleekDash\Controllers\Plugins;

/**
 * Reads and extracts changelog sections from plugin readme.txt files.
 */
class PluginChangelogService
{
    private const ERR_CHANGELOG_FILE_NOT_FOUND = 'Changelog file not found';

    private const ERR_DETAIL_NO_README_OR_CHANGELOG = 'No readme.txt or changelog file found for this plugin';

    /**
     * Get changelog for a plugin. Returns array with changelog section or error info.
     *
     * @param string $plugin_file Plugin file path (e.g. "fleekdash/fleekdash.php")
     * @return array{success: bool, changelog?: string, plugin_file?: string, plugin_slug?: string, source_file?: string, error?: string, error_data?: array}
     */
    public function getChangelog(string $plugin_file): array
    {
        $slug = $this->getPluginSlugFromPluginFile($plugin_file);
        if ($slug === null) {
            return $this->failureInvalidPluginFileParameter();
        }

        return $this->resolveChangelogForSlug($plugin_file, $slug);
    }

    /**
     * @return array{success: bool, changelog?: string, plugin_file?: string, plugin_slug?: string, source_file?: string, error?: string, error_data?: array}
     */
    private function resolveChangelogForSlug(string $plugin_file, string $slug): array
    {
        $lastFailure = null;
        $readme = $this->validateAndFindReadme($plugin_file);
        if ($readme['success'] ?? false) {
            $changelog = $this->extractChangelogFromReadme($readme['content']);
            if (!empty($changelog) && $changelog !== 'No changelog section found.') {
                return [
                    'success' => true,
                    'changelog' => $changelog,
                    'plugin_file' => $plugin_file,
                    'plugin_slug' => $slug,
                    'source_file' => basename($readme['path'] ?? 'readme.txt'),
                ];
            }
            $lastFailure = [
                'success' => false,
                'error' => 'Changelog section not found',
                'error_data' => ['error' => 'Changelog section not found in readme.txt', 'plugin_file' => $plugin_file],
            ];
        } else {
            $lastFailure = $readme;
        }

        $wpChangelog = $this->fetchPluginChangelogFromWordPressOrg($slug);
        if ($wpChangelog !== null && $wpChangelog !== '') {
            return [
                'success' => true,
                'changelog' => $wpChangelog,
                'plugin_file' => $plugin_file,
                'plugin_slug' => $slug,
                'source_file' => 'wordpress.org',
            ];
        }

        return $lastFailure ?? [
            'success' => false,
            'error' => self::ERR_CHANGELOG_FILE_NOT_FOUND,
            'error_data' => [
                'error' => self::ERR_DETAIL_NO_README_OR_CHANGELOG,
                'plugin_file' => $plugin_file,
                'plugin_slug' => $slug,
            ],
        ];
    }

    /**
     * @return array{success: false, error: string, error_data: array<string, mixed>}
     */
    private function failureInvalidPluginFileParameter(): array
    {
        return [
            'success' => false,
            'error' => 'Invalid plugin file parameter',
            'error_data' => ['error' => 'Plugin slug contains invalid characters'],
        ];
    }

    private function getPluginSlugFromPluginFile(string $plugin_file): ?string
    {
        $slug = \explode('/', $plugin_file)[0];
        if (empty($slug) || !\preg_match('/^[a-zA-Z0-9_-]+$/', $slug)) {
            return null;
        }

        return $slug;
    }

    /**
     * Fetch changelog HTML from api.wordpress.org when local readme has no section.
     */
    private function fetchPluginChangelogFromWordPressOrg(string $slug): ?string
    {
        $url = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=' . \rawurlencode($slug);
        $response = \wp_remote_get($url, ['timeout' => 12]);
        $changelog = null;
        if (!\is_wp_error($response)) {
            $code = (int) \wp_remote_retrieve_response_code($response);
            if ($code === 200) {
                $body = (string) \wp_remote_retrieve_body($response);
                $data = \json_decode($body, true);
                if (\is_array($data) && !empty($data['sections']['changelog']) && \is_string($data['sections']['changelog'])) {
                    $changelog = $data['sections']['changelog'];
                }
            }
        }

        return $changelog;
    }

    /**
     * Validate plugin slug and find readme file. Returns success with content or error array.
     *
     * @return array{success: bool, content?: string, path?: string, slug?: string, error?: string, error_data?: array}
     */
    private function validateAndFindReadme(string $plugin_file): array
    {
        $slug = \explode('/', $plugin_file)[0];
        if (empty($slug) || !\preg_match('/^[a-zA-Z0-9_-]+$/', $slug)) {
            $out = ['success' => false, 'error' => 'Invalid plugin file parameter', 'error_data' => ['error' => 'Plugin slug contains invalid characters']];
        } else {
            $paths = [
                \WP_PLUGIN_DIR . '/' . $slug . '/readme.txt',
                \WP_PLUGIN_DIR . '/' . $slug . '/README.txt',
                \WP_PLUGIN_DIR . '/' . $slug . '/changelog.txt',
                \WP_PLUGIN_DIR . '/' . $slug . '/CHANGELOG.txt',
                \WP_PLUGIN_DIR . '/' . $slug . '/CHANGELOG.md',
            ];
            $out = $this->changelogFileNotFoundFailure($plugin_file, $slug, $paths);
            foreach ($paths as $path) {
                if (file_exists($path) && is_readable($path)) {
                    $content = file_get_contents($path);
                    if ($content !== false) {
                        $out = ['success' => true, 'content' => $content, 'path' => $path, 'slug' => $slug];
                    } else {
                        $out = $this->changelogFileNotFoundFailure($plugin_file, $slug, $paths);
                    }
                    break;
                }
            }
        }

        return $out;
    }

    /**
     * @param list<string> $paths
     *
     * @return array{success: false, error: string, error_data: array<string, mixed>}
     */
    private function changelogFileNotFoundFailure(string $plugin_file, string $slug, array $paths): array
    {
        return [
            'success' => false,
            'error' => self::ERR_CHANGELOG_FILE_NOT_FOUND,
            'error_data' => [
                'error' => self::ERR_DETAIL_NO_README_OR_CHANGELOG,
                'plugin_file' => $plugin_file,
                'plugin_slug' => $slug,
                'searched_paths' => $paths,
            ],
        ];
    }

    /**
     * Extract changelog section from readme.txt content.
     *
     * @param string $content Full readme.txt content
     * @return string Changelog section or empty string
     */
    public function extractChangelogFromReadme(string $content): string
    {
        $changelog_start = strpos($content, '== Changelog ==');

        if ($changelog_start === false) {
            $changelog_start = strpos($content, '## Changelog');
        }
        if ($changelog_start === false) {
            $changelog_start = strpos($content, '# Changelog');
        }
        if ($changelog_start === false) {
            $changelog_start = strpos($content, 'CHANGELOG');
        }

        if ($changelog_start === false) {
            if ($this->looksLikeChangelogContent($content)) {
                return trim($content);
            }
            return 'No changelog section found.';
        }

        $after_changelog = substr($content, $changelog_start);

        $next_section = strpos($after_changelog, "\n== ");
        if ($next_section === false) {
            $next_section = strpos($after_changelog, "\n## ");
        }
        if ($next_section === false) {
            $next_section = strpos($after_changelog, "\n# ");
        }

        $changelog_content = ($next_section === false)
            ? $after_changelog
            : substr($after_changelog, 0, $next_section);

        $changelog_content = preg_replace('/^==\s*Changelog\s*==/i', '', $changelog_content);
        $changelog_content = preg_replace('/^##\s*Changelog/i', '', $changelog_content);
        $changelog_content = preg_replace('/^#\s*Changelog/i', '', $changelog_content);
        $changelog_content = preg_replace('/^CHANGELOG/i', '', $changelog_content);
        $changelog_content = trim($changelog_content);

        return $changelog_content;
    }

    private function looksLikeChangelogContent(string $content): bool
    {
        return (bool) preg_match('/^=+\s*\d+\.\d+/m', $content)
            || (bool) preg_match('/^#+\s*\d+\.\d+/m', $content)
            || (bool) preg_match('/^Version\s+\d+\.\d+/m', $content);
    }
}
