<?php
declare(strict_types=1);

namespace App\Core;

use App\Core\Database\ConnectionFactory;
use App\Core\Database\Migrator;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\I18n\Translator;
use App\Core\Routing\Router;
use App\Core\Support\Logger;
use App\Core\Support\Session;
use App\Core\View\Template;
use App\Modules\Auth\AuthService;
use App\Modules\Cron\CronHandlerFactory;
use App\Modules\Cron\CronRepository;
use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\OrderStatusRepository;
use App\Modules\Users\UserRepository;
use Throwable;
use PDO;

final class Application
{
    private Router $router;
    private Template $template;
    private AuthService $authService;
    private UserRepository $userRepository;
    private OrdersRepository $ordersRepository;
    private OrderStatusRepository $orderStatusRepository;
    private Migrator $migrator;
    private PDO $db;
    private Logger $logger;
    private Translator $translator;

    /**
     * @param array<string, array<string, mixed>> $config
     */
    public function __construct(
        private readonly string $basePath,
        private readonly array $config
    ) {
        $this->router = new Router();
        $this->translator = new Translator(
            (string) $this->config('app.lang_path'),
            (string) $this->config('app.locale', 'pl')
        );
        $this->template = new Template((string) $this->config('app.view_path'), $this->translator);
        $this->db = ConnectionFactory::make((array) $this->config('database', []));
        $this->userRepository = new UserRepository($this->db);
        $this->ordersRepository = new OrdersRepository($this->db);
        $this->orderStatusRepository = new OrderStatusRepository($this->db);
        $this->migrator = new Migrator(
            $this->db,
            (string) $this->config('app.migrations_path', $this->basePath('database/migrations'))
        );
        $this->authService = new AuthService($this->userRepository);
        $this->logger = new Logger((string) $this->config('app.log_path'));
    }

    public function boot(): void
    {
        $this->prepareDirectories();
        $this->configureSession();
        $this->registerErrorHandlers();

        $routes = require $this->basePath . '/routes/web.php';
        $routes($this);
    }

    public function run(): void
    {
        $request = Request::capture();
        $this->maybeRunCronOnWeb($request);
        $response = $this->router->dispatch($request);
        $response->send();
    }

    public function basePath(string $path = ''): string
    {
        if ($path === '') {
            return $this->basePath;
        }

        return $this->basePath . '/' . ltrim($path, '/');
    }

    public function router(): Router
    {
        return $this->router;
    }

    public function template(): Template
    {
        return $this->template;
    }

    public function auth(): AuthService
    {
        return $this->authService;
    }

    public function logger(): Logger
    {
        return $this->logger;
    }

    public function users(): UserRepository
    {
        return $this->userRepository;
    }

    public function orderStatuses(): OrderStatusRepository
    {
        return $this->orderStatusRepository;
    }

    public function orders(): OrdersRepository
    {
        return $this->ordersRepository;
    }

    public function db(): PDO
    {
        return $this->db;
    }

    public function migrator(): Migrator
    {
        return $this->migrator;
    }

    public function translator(): Translator
    {
        return $this->translator;
    }

    public function config(string $key, mixed $default = null): mixed
    {
        $segments = explode('.', $key);
        $value = $this->config;

        foreach ($segments as $segment) {
            if (!is_array($value) || !array_key_exists($segment, $value)) {
                return $default;
            }

            $value = $value[$segment];
        }

        return $value;
    }

    private function prepareDirectories(): void
    {
        $required = [
            $this->basePath('storage/logs'),
            $this->basePath('storage/sessions'),
            $this->basePath('storage/cache'),
            $this->basePath('storage/tmp'),
        ];

        foreach ($required as $directory) {
            if (!is_dir($directory)) {
                mkdir($directory, 0775, true);
            }
        }
    }

    private function configureSession(): void
    {
        $sessionName = (string) $this->config('app.session.name', 'orderpro_session');
        $sessionPath = (string) $this->config('app.session.path', $this->basePath('storage/sessions'));

        if (is_dir($sessionPath)) {
            session_save_path($sessionPath);
        }

        session_name($sessionName);
        Session::start();
    }

    private function registerErrorHandlers(): void
    {
        $debug = (bool) $this->config('app.debug', false);

        if ($debug) {
            ini_set('display_errors', '1');
            error_reporting(E_ALL);
        } else {
            ini_set('display_errors', '0');
            error_reporting(E_ALL);
        }

        set_exception_handler(function (Throwable $exception) use ($debug): void {
            $this->logger->error('Unhandled exception', [
                'message' => $exception->getMessage(),
                'file' => $exception->getFile(),
                'line' => $exception->getLine(),
            ]);

            $message = $debug ? $exception->getMessage() : 'Internal server error';
            Response::html($message, 500)->send();
        });

        set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
            $this->logger->error('PHP error', [
                'severity' => $severity,
                'message' => $message,
                'file' => $file,
                'line' => $line,
            ]);

            return false;
        });
    }

    private function maybeRunCronOnWeb(Request $request): void
    {
        $path = $request->path();
        if (
            $path === '/health'
            || $path === '/cron'
            || str_starts_with($path, '/cron/')
            || str_starts_with($path, '/assets/')
        ) {
            return;
        }

        try {
            $repository = new CronRepository($this->db);
            $runOnWeb = $repository->getBoolSetting(
                'cron_run_on_web',
                (bool) $this->config('app.cron.run_on_web_default', false)
            );
            if (!$runOnWeb) {
                return;
            }

            $webLimit = $repository->getIntSetting(
                'cron_web_limit',
                (int) $this->config('app.cron.web_limit_default', 5),
                1,
                100
            );

            if ($this->isWebCronThrottled(10)) {
                return;
            }
            if (!$this->acquireWebCronLock()) {
                return;
            }

            try {
                $factory = new CronHandlerFactory(
                    $this->db,
                    (string) $this->config('app.integrations.secret', ''),
                    $this->basePath()
                );
                $runner = $factory->build($repository, $this->logger);
                $runner->run($webLimit);
            } finally {
                $this->releaseWebCronLock();
            }
        } catch (Throwable $exception) {
            $this->logger->error('Web cron run failed', [
                'message' => $exception->getMessage(),
                'path' => $path,
            ]);
        }
    }

    private function isWebCronThrottled(int $minIntervalSeconds): bool
    {
        $safeInterval = max(1, $minIntervalSeconds);
        $now = time();
        $lastRunAt = $this->getWebCronLastRunAt();

        if ($lastRunAt > 0 && ($now - $lastRunAt) < $safeInterval) {
            return true;
        }

        $this->setWebCronLastRunAt($now);
        return false;
    }

    private function getWebCronLastRunAt(): int
    {
        try {
            $stmt = $this->db->prepare(
                "SELECT setting_value FROM app_settings WHERE setting_key = 'cron_web_last_run_at'"
            );
            $stmt->execute();
            $value = $stmt->fetchColumn();
            return $value !== false ? (int) $value : 0;
        } catch (Throwable) {
            return 0;
        }
    }

    private function setWebCronLastRunAt(int $timestamp): void
    {
        try {
            $this->db->prepare(
                "INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at)
                 VALUES ('cron_web_last_run_at', :ts, NOW(), NOW())
                 ON DUPLICATE KEY UPDATE setting_value = :ts2, updated_at = NOW()"
            )->execute(['ts' => (string) $timestamp, 'ts2' => (string) $timestamp]);
        } catch (Throwable) {
        }
    }

    private function acquireWebCronLock(): bool
    {
        $statement = $this->db->query("SELECT GET_LOCK('orderpro_web_cron_lock', 0)");
        $value = $statement !== false ? $statement->fetchColumn() : false;

        return (string) $value === '1';
    }

    private function releaseWebCronLock(): void
    {
        $this->db->query("DO RELEASE_LOCK('orderpro_web_cron_lock')");
    }
}
