<?php

namespace Acms\Services\Blog;

use Acms\Services\Facades\Database as DB;
use Acms\Services\Facades\Preview;
use SQL;
use ACMS_RAM;

class Helper
{
    /**
     * ライセンスされているドメインかチェック
     *
     * @param string $domain
     * @param int $aid
     * @param bool $isAlias
     * @param bool $update
     *
     * @return bool
     */
    public function isDomain($domain, $aid, $isAlias = false, $update = false)
    {
        // フォーマットチェック | ドメイン・IP・localhostのいずれか
        if (
            !preg_match(
                '@^([a-z0-9]+[a-z0-9-]*(?<=[a-z0-9])\.)+[a-z]+[a-z0-9-]*(?<=[a-z0-9])\.?$|' .
                '^(?:\d{1,3}\.){3}\d{1,3}$|' .
                '^localhost$@',
                $domain,
                $match
            )
        ) {
            return false;
        }

        // フォーマットチェックを通過済みで，エイリアスであり許可された共用SSLドメインなら true
        if ($isAlias && defined('LICENSE_SHARED_SSLDOMAIN')) {
            if ($domain == LICENSE_SHARED_SSLDOMAIN) {
                return true;
            }
        }

        if (preg_match(REGEX_VALID_IPV4_ADDR, $domain)) {
            return true;
        }

        // 独自ドメイン拡張 であれば true
        if (LICENSE_OPTION_OWNDOMAIN) {
            return true;
        }

        // サブドメイン拡張であれば，入力ドメインが登録ドメインのサブドメインであるかを判定
        if (LICENSE_OPTION_SUBDOMAIN && is_int(strpos($domain, LICENSE_DOMAIN))) {
            return true;
        }

        // 独自ドメイン拡張数内か判定
        if (defined('LICENSE_OPTION_PLUSDOMAIN') && intval(LICENSE_OPTION_PLUSDOMAIN) > 0 && $domain !== DOMAIN) {
            $count  = 0;
            $exsits = false;
            $dnAry  = [];

            $DB     = DB::singleton(dsn());
            $SQL    = SQL::newSelect('blog');
            $SQL->addSelect('blog_domain', null, null, 'DISTINCT');
            $SQL->addWhereOpr('blog_domain', DOMAIN, '<>');
            if ($update) {
                $SQL->addWhereOpr('blog_id', BID, '<>');
            }
            $all    = $DB->query($SQL->get(dsn()), 'all');
            foreach ($all as $blog) {
                $blog_domain = $blog['blog_domain'];
                if (preg_match(REGEX_VALID_IPV4_ADDR, $blog_domain)) {
                    continue;
                }
                $dnAry[] = $blog_domain;
            }

            $SQL    = SQL::newSelect('alias');
            $SQL->addSelect('alias_domain', null, null, 'DISTINCT');
            $SQL->addWhereOpr('alias_domain', DOMAIN, '<>');
            if ($update) {
                $SQL->addWhereOpr('alias_id', $aid, '<>');
            }
            $all    = $DB->query($SQL->get(dsn()), 'all');
            foreach ($all as $alias) {
                $alias_domain = $alias['alias_domain'];
                if (preg_match(REGEX_VALID_IPV4_ADDR, $alias_domain)) {
                    continue;
                }
                $dnAry[] = $alias_domain;
            }

            $dnAry = array_unique($dnAry);
            foreach ($dnAry as $dn) {
                if (LICENSE_OPTION_SUBDOMAIN && is_int(strpos($dn, LICENSE_DOMAIN))) {
                    continue;
                }
                if ($dn === $domain) {
                    $exsits = true;
                }
                $count++;
            }

            if ($exsits || intval(LICENSE_OPTION_PLUSDOMAIN) > $count) {
                return true;
            }
        }

        // 入力ドメインとconfig.serverのDOMAINが同一であるかを判定
        if ($domain === DOMAIN) {
            return true;
        }

        return false;
    }

    /**
     * ブログコードの存在をチェック
     *
     * @param string $domain
     * @param string $code
     * @param int $bid
     * @param int $aid
     *
     * @return array|bool
     */
    public function isCodeExists($domain, $code, $bid = null, $aid = null)
    {
        $DB     = DB::singleton(dsn());
        $domain = strval($domain);
        $code   = strval($code);

        //----------
        // category
        $SQL    = SQL::newSelect('category');
        $SQL->addLeftJoin('blog', 'blog_id', 'category_blog_id');
        $SQL->setSelect('category_id');
        $SQL->addWhereOpr('blog_domain', $domain);
        $SQL->addWhereOpr('blog_code', '');
        $SQL->addWhereOpr('category_code', $code);
        $SQL->setLimit(1);
        $res    = !$DB->query($SQL->get(dsn()), 'one');

        //------
        // blog
        if (!empty($res)) {
            $SQL    = SQL::newSelect('blog');
            $SQL->setSelect('blog_id');
            $SQL->addWhereOpr('blog_domain', $domain);
            $SQL->addWhereOpr('blog_code', $code);
            if (!empty($bid)) {
                $SQL->addWhereOpr('blog_id', $bid, '<>');
            }
            $SQL->setLimit(1);
            $res    = !$DB->query($SQL->get(dsn()), 'one');
        }

        //-------
        // alias
        if (!empty($res)) {
            $SQL    = SQL::newSelect('alias');
            $SQL->setSelect('alias_id');
            $SQL->addWhereOpr('alias_domain', $domain);
            $SQL->addWhereOpr('alias_code', $code);
            if (!empty($aid)) {
                $SQL->addWhereOpr('alias_id', $aid, '<>');
            }
            $SQL->setLimit(1);
            $res    = !$DB->query($SQL->get(dsn()), 'one');
        }

        //--------------
        // global alias
        if (!empty($res)) {
            $SQL    = SQL::newSelect('alias');
            $SQL->addSelect('alias_code');
            $SQL->addWhereOpr('alias_scope', 'global');
            $SQL->addWhereOpr('alias_domain', $domain);
            if ($all = $DB->query($SQL->get(dsn()), 'all')) {
                foreach ($all as $acode) {
                    if ($acode == $code) {
                        return false;
                    }
                }
            }
            return true;
        }
        return $res;
    }

    /**
     * 指定したブログのステータスが設定できるか
     *
     * @param string $val
     * @param bool $update
     *
     * @return string|bool
     */
    public function isValidStatus($val, $update = false)
    {
        if (empty($val)) {
            return true;
        }
        if ('close' == $val) {
            return true;
        }

        // 子ブログが祖先より公開度を上げないようにする。
        // 「open に変更したい」場合は祖先に secret / close が存在してはならない。
        // 「secret に変更したい」場合は祖先に close が存在してはならない。
        $aryStatus  = [];
        switch ($val) {
            case 'open':
                $aryStatus[]    = 'secret';
                $aryStatus[]    = 'close';
                break;
            case 'secret':
                $aryStatus[]    = 'close';
                break;
            case 'close':
            default:
        }

        $DB     = DB::singleton(dsn());
        $SQL    = SQL::newSelect('blog');
        $SQL->setSelect('blog_id');
        $SQL->addWhereOpr('blog_left', ACMS_RAM::blogLeft(BID), '<');
        $SQL->addWhereOpr('blog_right', ACMS_RAM::blogRight(BID), '>');
        $SQL->addWhereIn('blog_status', $aryStatus);
        $SQL->setLimit(1);

        return !$DB->query($SQL->get(dsn()), 'one');
    }

    /**
     * 指定したブログが子孫ブログを持っているか
     * @param int $blogId
     * @return bool
     */
    public function hasDescendantBlogs(int $blogId): bool
    {
        if (1 < ACMS_RAM::blogRight($blogId) - ACMS_RAM::blogLeft($blogId)) {
            return true;
        }
        return false;
    }

    /**
     * 現在ログインしているユーザーが指定ブログ配下に新規ブログを作成できるか
     *
     * $blogId には新規ブログの親となるブログ ID（通常は現在の BID）を渡す。
     * ライセンスチェック・スーパーブログ要件など a-blog cms 全体のドメインルールも含む。
     * ショートカット権限による許可はモジュール側で別途検証する。
     * @param int $blogId
     * @return bool
     */
    public function canCreate(int $blogId): bool
    {
        if ($blogId <= 0) {
            return false;
        }
        if (Preview::isPreviewMode()) {
            return false;
        }
        $sessionUserBlogId = $this->resolveSessionUserBlogId();
        if ($sessionUserBlogId === null) {
            return false;
        }
        if (defined('IS_LICENSED') && !IS_LICENSED) {
            return false;
        }
        if (!isBlogGlobal($sessionUserBlogId)) {
            // ログインユーザーの所属ブログに子ブログが1つも存在しない場合は新規ブログ作成を許可しない
            return false;
        }
        if (sessionWithAdministration($blogId)) {
            return true;
        }
        return false;
    }

    /**
     * 現在ログインしているユーザーが指定ブログを編集できるか
     *
     * ショートカット権限による許可はモジュール側で別途検証する。
     * @param int $blogId
     * @return bool
     */
    public function canUpdate(int $blogId): bool
    {
        if ($blogId <= 0) {
            return false;
        }
        if (Preview::isPreviewMode()) {
            return false;
        }
        if (sessionWithAdministration($blogId)) {
            return true;
        }
        return false;
    }

    /**
     * 現在ログインしているユーザーが指定ブログを削除できるか
     *
     * スーパーブログ自身は削除できない。現在のスーパーブログがグローバルブログ
     * （ドメイン直下）でない場合も削除を許可しない。
     * @param int $blogId
     * @return bool
     */
    public function canDelete(int $blogId): bool
    {
        if ($blogId <= 0) {
            return false;
        }
        $sessionUserBlogId = $this->resolveSessionUserBlogId();
        if ($sessionUserBlogId === null) {
            return false;
        }
        if (Preview::isPreviewMode()) {
            return false;
        }
        if ($blogId === $sessionUserBlogId) {
            return false;
        }
        if (!isBlogGlobal($sessionUserBlogId)) {
            // ログインユーザーの所属ブログに子ブログが1つも存在しない場合は削除を許可しない
            return false;
        }
        if (sessionWithAdministration($blogId)) {
            return true;
        }
        return false;
    }

    /**
     * 現在ログインしているユーザーが指定ブログのメンテナンスモードを切り替えできるか
     * @param int $blogId
     * @return bool
     */
    public function canChangeMaintenanceMode(int $blogId): bool
    {
        if ($blogId <= 0) {
            return false;
        }
        if (Preview::isPreviewMode()) {
            return false;
        }
        if (sessionWithAdministration($blogId)) {
            return true;
        }
        return false;
    }

    /**
     * 現在ログインしているユーザーが指定ブログ配下のツリー / コンフィグセット等の一括操作を行えるか
     *
     * 並び替え (Sort)、親変更 (Parent)、コンフィグセット一括設定 (ConfigSet) で利用する。
     * @param int $blogId
     * @return bool
     */
    public function canManageStructure(int $blogId): bool
    {
        if ($blogId <= 0) {
            return false;
        }
        if (Preview::isPreviewMode()) {
            return false;
        }
        if (sessionWithAdministration($blogId)) {
            return true;
        }
        return false;
    }

    /**
     * 現在ログインしているユーザーが指定ブログ配下のステータス一括変更を行えるか
     *
     * セッション権限と業務ルール（{@see canTransitionStatus()}）の両方を満たす場合に true。
     * @param int $blogId
     * @param string $toStatus
     * @return bool
     */
    public function canBulkChangeStatus(int $blogId, string $toStatus): bool
    {
        if ($blogId <= 0) {
            return false;
        }
        if (Preview::isPreviewMode()) {
            return false;
        }
        if (!sessionWithAdministration($blogId)) {
            return false;
        }
        if (!$this->canTransitionStatus($blogId, $toStatus)) {
            return false;
        }
        return true;
    }

    /**
     * 指定ブログから $toStatus へのステータス遷移が業務ルール上許可されるか
     *
     * 現在のステータスより公開度を上げる方向への変更は禁止する:
     *   - 現在 open   → 何にでも変更可
     *   - 現在 secret → secret / close へ変更可 (open は不可)
     *   - 現在 close  → 変更不可
     * @param int $blogId
     * @param string $toStatus
     * @return bool
     */
    public function canTransitionStatus(int $blogId, string $toStatus): bool
    {
        $currentStatus = ACMS_RAM::blogStatus($blogId);
        if ($currentStatus === 'open') {
            return true;
        }
        if ($currentStatus === 'secret') {
            if ($toStatus === 'open') {
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * セッションユーザーが所属するブログ ID を取得する
     *
     * SBID は main.php で実行時に決まる動的グローバル定数で、未確定の場合は null になり得る。
     * 値が正の整数として確定している場合のみ返す。
     */
    private function resolveSessionUserBlogId(): ?int
    {
        if (!defined('SBID')) {
            return null;
        }
        /** @var int|null $value */
        $value = SBID;
        if (!is_int($value)) {
            return null;
        }
        if ($value <= 0) {
            return null;
        }
        return $value;
    }
}
