<?php

namespace Sitecake\Services;

use Sitecake\Authentication\Authenticate;
use Sitecake\Authentication\Authenticator\DefaultAuthenticator;
use Sitecake\Authentication\Authenticator\SessionAuthenticator;
use Sitecake\Authentication\CredentialManager;
use Sitecake\Authentication\Identifier\CredentialFileIdentifier;
use Sitecake\Content\PageRenderAwareTrait;
use Sitecake\Exception\MissingArgumentsException;
use Sitecake\Http\Session;
use Sitecake\License;
use Sitecake\PageManager\Page;
use Sitecake\PageManager\PageManager;
use Sitecake\Resources\ResourceManager;
use Sitecake\SessionManager;
use Sitecake\Site;
use Sitecake\Sitecake as App;
use Sitecake\Http\Request;

class SessionService extends Service
{
    use PageRenderAwareTrait;

    const SUCCESS_SESSION_STARTED = 0;
    const FAILURE_AUTHENTICATION_FAILED = 1;
    const FAILURE_SESSION_EXISTS = 2;
    const FAILURE_LICENSE_INVALID = 3;

    const CREDENTIAL_PARAM_NAME = 'credentials';

    /**
     * @var Authenticate
     */
    protected $auth;

    /**
     * @var CredentialManager
     */
    protected $credentialManager;

    /**
     * @var SessionManager
     */
    protected $sessionManager;

    /**
     * @var PageManager
     */
    protected $pageManager;

    /**
     * @var ResourceManager
     */
    protected $resourceManager;

    /**
     * SessionService constructor.
     *
     * @param Site   $site
     * @param string $alias
     * @param array  $config
     */
    public function __construct(Site $site, $alias, array $config = [])
    {
        parent::__construct($site, $alias, $config);
        $this->initializeAuth($this->getConfig('CREDENTIALS_PATH'));
        $this->sessionManager = $this->getConfig('sessionManager');
        $this->pageManager = $this->getConfig('pageManager');
        $this->resourceManager = $this->getConfig('resourceManager');
    }

    /**
     * {@inheritdoc}
     */
    public function isAuthRequired($action): bool
    {
        return !($action === 'login' || $action === 'change');
    }

    /**
     * Initialize authentication
     *
     * @param string $credentialsPath
     *
     * @return void
     */
    protected function initializeAuth($credentialsPath)
    {
        $this->auth = new Authenticate();
        $this->auth->registerAuthenticator(new SessionAuthenticator());
        if (file_exists($credentialsPath)) {
            $this->credentialManager = new CredentialManager($credentialsPath);
            $this->auth->registerAuthenticator(
                new DefaultAuthenticator(new CredentialFileIdentifier($this->credentialManager), [
                    'paramName' => self::CREDENTIAL_PARAM_NAME
                ])
            );
        }
    }

    /**
     * Login action.
     * Authenticates user and starts editing session if not already started
     *
     * @param Request $request
     *
     * @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response
     * @throws \Exception
     */
    public function login(Request $request)
    {
        if ($request->request->get(self::CREDENTIAL_PARAM_NAME)) {
            // Rate limit before any authentication work
            if (!$this->checkLoginRateLimit()) {
                usleep(500000);
                return $this->json($request, ['status' => self::FAILURE_AUTHENTICATION_FAILED], 429);
            }

            $response = self::FAILURE_AUTHENTICATION_FAILED;
            if ($this->auth->authenticate($request)) {
                $this->clearLoginRateLimit();

                // Handle license enforcement on real domains
                if (!License::isLocal(License::currentDomain())) {
                    $licenseValid = License::check(App::getPath('LICENSE_FILE_PATH'));
                    if (!$licenseValid) {
                        $licenseKey = trim($request->request->get('licenseKey') ?? '');
                        if ($licenseKey === '') {
                            return $this->json($request, [
                                'status'     => self::FAILURE_LICENSE_INVALID,
                                'errMessage' => 'A license key is required to log in on this domain.',
                            ], 200);
                        }
                        $licenseResult = License::activate($licenseKey, App::getPath('LICENSE_FILE_PATH'));
                        if ($licenseResult['status'] !== 0) {
                            return $this->json($request, [
                                'status'     => self::FAILURE_LICENSE_INVALID,
                                'errMessage' => $licenseResult['message'] ?? 'Invalid license key',
                            ], 200);
                        }
                    }
                }

                if ($this->sessionManager->sessionExists()) {
                    $response = self::FAILURE_SESSION_EXISTS;
                } else {
                    // Fix session fixation — regenerate session ID on login
                    session_regenerate_id(true);
                    $request->getSession()->set('loggedin', true);
                    $this->sessionManager->startSession();

                    $response = self::SUCCESS_SESSION_STARTED;
                }
            } else {
                $this->recordLoginFailure();
                usleep(200000); // 200ms delay to slow brute force
            }

            return $this->json($request, ['status' => $response], 200);
        } else {
            $page = $this->pageManager->getPage();

            $this->preparePageForRender($page);

            $this->injectLoginDialog($page);

            $htmlResponse = $this->response($page->render());
            $htmlResponse->headers->set('Content-Type', 'text/html; charset=UTF-8');
            return $htmlResponse;
        }
    }

    /**
     * Adds needed scripts for login to page
     *
     * @param Page $page
     */
    protected function injectLoginDialog(Page $page)
    {
        $page->enqueueStyle(App::getPath('EDITOR_CSS_URL'))
            ->enqueueScript($this->getLoginClientCode(), ['inline' => true])
            ->enqueueScript(App::getPath('EDITOR_JS_URL'), [], ['data-cfasync' => 'false']);
    }

    /**
     * Returns js code that needs to be injected on login page
     *
     * @return string
     */
    protected function getLoginClientCode()
    {
        $domain          = License::currentDomain();
        $requiresLicense = !License::isLocal($domain);
        $licenseValid    = License::check(App::getPath('LICENSE_FILE_PATH'));

        return 'var sitecakeGlobals = {' .
            "editMode: false, " .
            'serverVersionId: "' . SITECAKE_VERSION . '", ' .
            'serviceUrl:"' . App::getPath('SERVICE_URL') . '", ' .
            'configUrl:"' . App::getPath('EDITOR_CONFIG_URL') . '", ' .
            'csrfToken: "' . Session::getCsrfToken() . '", ' .
            'requiresLicense: ' . ($requiresLicense ? 'true' : 'false') . ', ' .
            'licenseValid: ' . ($licenseValid ? 'true' : 'false') . ', ' .
            'forceLoginDialog: true' .
            '};';
    }

    /**
     * Change action.
     * Changes password — always re-verifies the current password regardless of session state.
     *
     * @param Request $request
     *
     * @return \Symfony\Component\HttpFoundation\JsonResponse
     */
    public function change(Request $request)
    {
        $credentials    = $request->request->get('credentials');
        $newCredentials = $request->request->get('newCredentials');

        if (is_null($credentials)) {
            throw new MissingArgumentsException(['name' => 'credentials'], 400);
        }

        if (is_null($newCredentials)) {
            throw new MissingArgumentsException(['name' => 'newCredentials'], 400);
        }

        // INFO-2: Always explicitly verify the current password with the credential identifier,
        // regardless of session state, to prevent an already-logged-in attacker from changing
        // the password without knowing the current one.
        $identifier = new CredentialFileIdentifier($this->credentialManager);
        if ($identifier->identify(['password' => $credentials])) {
            $this->credentialManager->saveCredentials(['password' => $newCredentials]);
            $status = 0;
        } else {
            $status = 1;
        }

        return $this->json($request, ['status' => $status], 200);
    }

    /**
     * Logout action.
     * Terminates editing session
     *
     * @param Request $request
     *
     * @return \Symfony\Component\HttpFoundation\JsonResponse
     */
    public function logout(Request $request)
    {
        $this->sessionManager->terminate();

        return $this->json($request, ['status' => 0], 200);
    }

    /**
     * Alive action.
     * Refresh editing session for current logged in user.
     *
     * @param Request $request
     *
     * @return \Symfony\Component\HttpFoundation\JsonResponse
     */
    public function alive(Request $request)
    {
        $this->sessionManager->refresh();

        return $this->json($request, ['status' => 0], 200);
    }

    // ── Login rate limiting ────────────────────────────────────────────────────

    private function checkLoginRateLimit(): bool
    {
        $ip  = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $key = sys_get_temp_dir() . '/sc_login_' . md5($ip);
        $data = @file_get_contents($key);
        $attempts = $data ? json_decode($data, true) : ['count' => 0, 'reset_at' => time() + 300];

        if (time() > $attempts['reset_at']) {
            $attempts = ['count' => 0, 'reset_at' => time() + 300];
        }

        return $attempts['count'] < 10;
    }

    private function recordLoginFailure(): void
    {
        $ip  = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $key = sys_get_temp_dir() . '/sc_login_' . md5($ip);
        $data = @file_get_contents($key);
        $attempts = $data ? json_decode($data, true) : ['count' => 0, 'reset_at' => time() + 300];

        if (time() > $attempts['reset_at']) {
            $attempts = ['count' => 0, 'reset_at' => time() + 300];
        }
        $attempts['count']++;
        file_put_contents($key, json_encode($attempts), LOCK_EX);
    }

    private function clearLoginRateLimit(): void
    {
        $ip  = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $key = sys_get_temp_dir() . '/sc_login_' . md5($ip);
        @unlink($key);
    }
}
