<?php

namespace App\Browser;

use App\Models\Job;
use App\Models\JobArtifact;
use App\Models\JobRun;
use App\Notification\Notifications\JobDebugNotification;
use App\Notification\Notifications\JobErrorNotification;
use App\Notification\Providers\AllNotification;
use Closure;
use Exception;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Chrome\SupportsChrome;
use Laravel\Dusk\Concerns\ProvidesBrowser;
use Log;
use PHPUnit\Framework\Attributes\BeforeClass;
use Throwable;

abstract class BrowserJob implements ShouldQueue
{
    use SupportsChrome, ProvidesBrowser, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $jobId;

    public $timeout = 300; // 5 minutes

    public function __construct(int $jobId)
    {
        $this->jobId = $jobId;
    }

    /**
     * Execute the callback in a browser
     * @param Closure $callback function with a Browser as parameter
     * @return void
     */
    private function executeInBrowser(Closure $callback): ?JobRun
    {

        $this->prepare();
        $this->setUp();

        $this->browse(function (Browser $browser) use ($callback, &$log) {
            try {
                $browser->driver->manage()->timeouts()->implicitlyWait(20);
                $log = $callback($browser);
            // } catch (Exception $e) {
            //     $browser->screenshot("failure-{$this->jobId}");
            //     dump($e);
            //     throw $e;
            }
            catch (Throwable $e) {
                $browser->screenshot(JobErrorScreenshot::getFileName($this->jobId));
                AllNotification::send(new JobErrorNotification($this->jobId, $e->getMessage()));
                dump($e);
                throw $e;
            } finally {
                $browser->quit();
                return [];
            }
        });

        return $log;
    }

    /**
     * Execute the job
     * @return void
     */
    public function execute(): ?JobRun {
        return $this->executeInBrowser(function (Browser $browser): ?JobRun {
            return $this->run($browser);
        });
    }

    /**
     * Execute the job test
     * @return void
     */
    public function executeTest(): ?JobRun {
        return $this->executeInBrowser(function (Browser $browser): ?JobRun {
            return $this->runTest($browser);
        });
    }

    /**
     * Steps that run in the browser
     * @param \Laravel\Dusk\Browser $browser
     * @return void
     */
    abstract public function run(Browser $browser): ?JobRun;

    abstract public function runTest(Browser $browser): ?JobRun;

    /**
     * Prepare for Dusk test execution.
     * @unused
     */
    #[BeforeClass]
    public static function prepare(): void
    {
        if (config("dusk.driver.url") == null && !(isset($_ENV['LARAVEL_SAIL']) && $_ENV['LARAVEL_SAIL'] == '1')) {
            static::startChromeDriver(['--port=9515']);
        }
    }

    /**
     * Register the base URL with Dusk.
     *
     * @return void
     */
    protected function setUp(): void
    {
        Browser::$baseUrl = "https://pdftools.matthiasg.dev/";

        Browser::$storeScreenshotsAt = base_path('app/Browser/screenshots');

        Browser::$storeConsoleLogAt = base_path('app/Browser/console');

        Browser::$storeSourceAt = base_path('app/Browser/source');

        /*Browser::$userResolver = function () {
            return $this->user();
        }; */
    }

    protected function makeSimpleJobRun(bool $success, string $name, string $content): JobRun {
        $artifact = new JobRun([
            "job_id" => $this->jobId,
            "success" => $success
        ]);
        $artifact->save();
        $artifact->artifacts()->save(new JobArtifact([
            "name" => $name,
            "content" => $content,
        ]));
        return $artifact;
    }

    /**
     * Create the RemoteWebDriver instance.
     */
    protected function driver(): RemoteWebDriver
    {
        $options = (new ChromeOptions)->addArguments(collect([
            $this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1360,1020',
            '--disable-search-engine-choice-screen',
            '--disable-gpu',
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '--whitelisted-ips=""',
            '--disable-dev-shm-usage',
            '--user-data-dir=/home/seluser/profile', // seems that selenium doesn't like docker having a volume on the exact same folder ("session not created: probably user data directory is already in use"). Does not seems like it, maybe whn the browser change version ?
        ])->all());

        try {
            return RemoteWebDriver::create(
                    config("dusk.driver.url", 'http://localhost:9515'),
                    DesiredCapabilities::chrome()->setCapability(
                        ChromeOptions::CAPABILITY,
                        $options
                )
                ->setCapability('timeouts', [
                    'implicit' => 20000, // 20 seconds
                    'pageLoad' => 300000, // 5 minutes
                    'script' => 30000, // 30 seconds
                ]),
                4000,
                $this->timeout * 1000
            );
        } catch (Exception $e) {
            Log::error("Failed to create RemoteWebDriver: " . $e->getMessage());
            AllNotification::send(new JobDebugNotification($this->jobId, "Failed to create RemoteWebDriver: " . $e->getMessage()));
            throw $e;
        }
    }

    public function terminate() {
        $this->browse(function (Browser $browser) {
            $browser->quit();
        });
    }

    /**
     * Determine whether the Dusk command has disabled headless mode.
     */
    protected function hasHeadlessDisabled(): bool
    {
        return config('dusk.headlessDisabled', false);
    }

    /**
     * Determine if the browser window should start maximized.
     */
    protected function shouldStartMaximized(): bool
    {
        return config('dusk.shouldStartMaximized', false);
    }

    /**
     * Register an "after class" tear down callback.
     *
     * @param  \Closure  $callback
     * @return void
     */
    public static function afterClass(Closure $callback)
    {
        static::$afterClassCallbacks[] = $callback;
    }

    public static function name()
    {
        return "test";
    }

    public static function dataName()
    {
        return "dataTest";
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        if (Job::find($this->jobId)->is_active) {
            $this->execute();
        }
    }

    public function reschedule($minutes) {
        $this::dispatch()->delay(now()->addMinutes($minutes));
    }


    // === BROWSER MACROS ===
    protected function waitForAndClickText(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true) {
        $browser->waitForText($text, $timeout, $ignoreCase);
        $this->findElementContainingText($browser, $text, $ignoreCase)?->click();
    }

    protected function waitForElementContainingTextAndGetIt(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true): RemoteWebElement|null {
        try {
            $browser->waitForText($text, $timeout, $ignoreCase);
            return $this->findElementContainingText($browser, $text, $ignoreCase);
        } catch (Exception $e) {
            return null;
        }
    }

    protected function findElementContainingText(Browser $browser, string $text, bool $ignoreCase = true): RemoteWebElement|null {
        try {
            if ($ignoreCase) {
                return $browser->driver->findElement(WebDriverBy::xpath("//*[{$this->xpathContainsIgnoreCase($text)}]"));
            } else {
                return $browser->driver->findElement(WebDriverBy::xpath("//*[contains(text(), \"{$text}\")]"));
            }
        } catch (Exception $e) {
            return null;
        }
    }

    private function xpathContainsIgnoreCase(string $needle, string $haystack = "text()") {
        $needle = strtolower($needle);
        return "contains(translate({$haystack}, \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\", \"abcdefghijklmnopqrstuvwxyz\"), \"{$needle}\")";
    }

    protected function waitForAndClickElementContainingText(Browser $browser, string $elementXpath, string $text, int $timeout = 30, bool $ignoreCase = true) {
        $browser->waitForText($text, $timeout, $ignoreCase);
        $this->findElementContainingElementWithText($browser, $elementXpath, $text, $ignoreCase)?->click();
    }

    protected function waitForElementContainingElementWithTextAndGetIt(Browser $browser, string $elementXpath, string $text, int $timeout = 30, bool $ignoreCase = true): RemoteWebElement|null {
        try {
            $browser->waitForText($text, $timeout, $ignoreCase);
            sleep(2);
            return $this->findElementContainingElementWithText($browser, $elementXpath, $text, $ignoreCase);
        } catch (Exception $e) {
            return null;
        }
    }

    protected function findElementContainingElementWithText(Browser $browser, string $elementXpath, string $text, bool $ignoreCase = true): RemoteWebElement|null {
        try {
            if ($ignoreCase) {
                return $browser->driver->findElement(WebDriverBy::xpath("{$elementXpath}[.//*[{$this->xpathContainsIgnoreCase($text)}]]"));
            } else {
                return $browser->driver->findElement(WebDriverBy::xpath("{$elementXpath}[.//*[contains(text(), \"{$text}\")]]"));
            }
        } catch (Exception $e) {
            return null;
        }
    }

    /**
     * Parse a string for use in JavaScript.
     *
     * @param string $string The string to parse.
     * @return string The parsed string.
     */
    private function parseJavaScriptString(string $string): string
    {
        $string = str_replace("\n", "\\n", $string);
        return str_replace("'", "\\'", $string);
    }

    /**
     * Type text into an input field using JavaScript.
     *
     * @param Browser $browser
     * @param string $text The text to type.
     * @param string $querySelector The CSS selector for the input field.
     */
    public function setInputValue(Browser $browser, string $text, string $querySelector): void
    {
        $text = $this->parseJavaScriptString($text);
        $querySelector = $this->parseJavaScriptString($querySelector);

        $browser->script("
            let element = document.querySelector('{$querySelector}');
            if (element) {
                element.focus();
                element.value = '{$text}';
                element.dispatchEvent(new Event('input', { bubbles: true }));
            } else {
                console.error('Element not found: {$querySelector}');
            }
        ");
    }

    /**
     * Paste text into an element using JavaScript.
     * Can be useful for non input elements that need to get text. For example works
     * with `contenteditable` elements.
     *
     * @param Browser $browser
     * @param string $text The text to paste.
     * @param string $querySelector The CSS selector for the input field.
     */
    public function pasteText(Browser $browser, string $text, string $querySelector): void
    {
        try {
            $text = $this->parseJavaScriptString($text);
            $querySelector = $this->parseJavaScriptString($querySelector);

            $browser->script("
                var el = document.querySelector('{$querySelector}'), text = '{$text}';
                el.focus();
                const dataTransfer = new DataTransfer();
                dataTransfer.setData('text', text);
                const event = new ClipboardEvent('paste', {
                    clipboardData: dataTransfer,
                    bubbles: true
                    });
                    el.dispatchEvent(event)
            ");
        } catch (Exception $e) {
            $errorMessage = "Failed to paste text into element: {$querySelector} - " . $e->getMessage() . "\n With text: {$text}";
            Log::error($errorMessage);
            $browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
            AllNotification::send(new JobDebugNotification($this->jobId, $errorMessage));
        }
    }

    public function clickElementWithJavaScript(Browser $browser, string $xPathSelector): void
    {
        $browser->script("
            var element = document.evaluate('{$xPathSelector}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
            if (element) {
                element.click();
            } else {
                console.error('Element not found: {$xPathSelector}');
            }
        ");
    }
}
