<?php

declare(strict_types=1);

namespace Acms\Services\Field;

use Acms\Services\Facades\Database as DB;
use Acms\Services\Facades\Entry;
use Acms\Services\Facades\Cache;
use Acms\Services\Facades\Logger as AcmsLogger;
use Acms\Services\Facades\PublicStorage;
use Acms\Services\Facades\Template as Tpl;
use SQL;
use Field;

/**
 * カスタムフィールドの永続化（取得・保存・削除）を担う。
 *
 * 画像/ファイル型フィールドの物理ファイルの回収は {@see FieldFileGarbageCollector} に委譲する。
 * DELETE → INSERT 完了後に、どこからも参照されなくなったものだけが削除される。
 *
 * Field ファサードの実体。POST → Field 変換（extract）は FieldExtractor に委譲する
 * （責務は分離しつつ、ファサード利用者からは save/load/delete/extract を単一の窓口で呼べるようにする）。
 */
class FieldRepository
{
    /**
     * @var \Acms\Services\Cache\Contracts\AdapterInterface
     */
    private $cacheField;

    /**
     * @var FieldExtractor
     */
    private $extractor;

    /**
     * 物理ファイルの回収担当
     *
     * @var FieldFileGarbageCollector
     */
    private $fileGarbageCollector;

    /**
     * @param FieldExtractor $extractor
     * @param FieldFileGarbageCollector|null $fileGarbageCollector 省略時はフック発火なしの既定インスタンス
     */
    public function __construct(FieldExtractor $extractor, ?FieldFileGarbageCollector $fileGarbageCollector = null)
    {
        $this->cacheField = Cache::field();
        $this->extractor = $extractor;
        $this->fileGarbageCollector = $fileGarbageCollector ?? new FieldFileGarbageCollector();
    }

    /**
     * POSTデータからデータの抜き出し
     *
     * @param string $scp
     * @param \ACMS_Validator|null $V
     * @param \Field|null $deleteField
     * @return \Field_Validation
     */
    public function extract($scp = 'field', $V = null, $deleteField = null)
    {
        return $this->extractor->extract($scp, $V, $deleteField);
    }

    /**
     * POSTデータからデータを抜き出し、抽出結果と削除フィールドをまとめて取得する
     *
     * @param string $scp
     * @param \ACMS_Validator|null $V
     * @param \Field|null $deleteField
     * @return ExtractResult
     */
    public function extractResult($scp = 'field', $V = null, $deleteField = null): ExtractResult
    {
        return $this->extractor->extractResult($scp, $V, $deleteField);
    }

    /**
     * extract()後の削除フィールドを取得
     *
     * @deprecated extractResult() の戻り値から deleteField() を取得してください。
     *
     * @return \Field
     */
    public function getDeleteField()
    {
        return $this->extractor->getDeleteField();
    }

    /**
     * カスタムフィールドキャッシュの削除
     *
     * @param 'bid'|'uid'|'cid'|'mid'|'eid'|'unit_id' $type
     * @param ($type is 'unit_id' ? string : int) $id
     * @param int|null $rvid
     */
    public function deleteFieldCache($type, $id, $rvid = null)
    {
        // Why not 移設前の `if ($type) { ... }` を残さない: 各変数の代入がこのガードの内側にあり、
        // $type が falsy だと未定義変数のまま下のキー連結に流れて Undefined variable Warning が 6 回出る。
        // 6 つの三項演算子はいずれも $type との比較なので、無条件に評価しても falsy な $type では
        // 全て '' になり、生成されるキャッシュキーは移設前と同一（Warning だけが消える）。
        $cacheBid = $type === 'bid' ? $id : '';
        $cacheUid = $type === 'uid' ? $id : '';
        $cacheCid = $type === 'cid' ? $id : '';
        $cacheMid = $type === 'mid' ? $id : '';
        $cacheEid = $type === 'eid' ? $id : '';
        $cacheUnitId = $type === 'unit_id' ? $id : '';
        $cacheKey = "cache-field-bid_{$cacheBid}-uid_{$cacheUid}-cid_{$cacheCid}-mid_{$cacheMid}-eid_{$cacheEid}-unitId_{$cacheUnitId}-rvid_{$rvid}-";

        $this->cacheField->forget("{$cacheKey}0-v1");
        $this->cacheField->forget("{$cacheKey}1-v1");
        $this->cacheField->forget("{$cacheKey}0-v2");
        $this->cacheField->forget("{$cacheKey}1-v2");
    }

    public function flushCache()
    {
        $this->cacheField->flush();
    }

    /**
     * カスタムフィールドの削除
     *
     * @param 'bid'|'uid'|'cid'|'mid'|'eid'|'unit_id' $type
     * @param ($type is 'unit_id' ? string : int) $id
     * @param int|null $rvid
     * @param int|null $blogId
     * @return void
     */
    public function delete($type, $id, $rvid = null, $blogId = null)
    {
        $this->deleteFieldCache($type, $id, $rvid);

        if (in_array($type, ['eid', 'unit_id'], true) && $rvid) {
            $sql = SQL::newDelete('field_rev');
            $sql->addWhereOpr('field_eid', $id);
            $sql->addWhereOpr('field_rev_id', $rvid);
            if ($blogId !== null) {
                $sql->addWhereOpr('field_blog_id', $blogId);
            }
            DB::query($sql->get(dsn()), 'exec');
        } else {
            $sql = SQL::newDelete('field');
            $sql->addWhereOpr('field_' . $type, $id);
            if ($blogId !== null) {
                $sql->addWhereOpr('field_blog_id', $blogId);
            }
            DB::query($sql->get(dsn()), 'exec');
        }
    }

    /**
     * ブログID, カテゴリーID, エントリーID，ユーザーIDの
     * いずれか指定されたカスタムフィールドをFieldオブジェクトで返す
     *
     * @param null|int $bid
     * @param null|int $uid
     * @param null|int $cid
     * @param null|int $mid
     * @param null|int $eid
     * @param null|string $unitId
     * @param null|int $rvid
     * @param bool $rewrite
     * @return Field
     */
    public function load($bid = null, $uid = null, $cid = null, $mid = null, $eid = null, $unitId = null, $rvid = null, $rewrite = false)
    {
        $cacheKey = "cache-field-bid_{$bid}-uid_{$uid}-cid_{$cid}-mid_{$mid}-eid_{$eid}-unitId_{$unitId}-rvid_{$rvid}-";
        $cacheKey .= ($rewrite ? '1' : '0');
        $cacheKey .= (isApiBuildOrV2Module() ? '-v2' : '-v1');

        $cacheItem = $this->cacheField->getItem($cacheKey);
        if ($cacheItem && $cacheItem->isHit()) {
            $cacheData = $cacheItem->get();
            if ($cacheData instanceof Field) {
                Tpl::injectMediaField($cacheData, force: true);
                return $cacheData;
            }
            $this->cacheField->forget($cacheKey);
        }
        $field = new Field();
        if (
            is_null($bid) &&
            is_null($uid) &&
            is_null($cid) &&
            is_null($eid) &&
            is_null($mid) &&
            is_null($unitId)
        ) {
            return $field;
        }
        if ($rvid && ($eid || $unitId)) {
            $SQL = SQL::newSelect('field_rev');
            $SQL->addWhereOpr('field_rev_id', $rvid);
        } else {
            $SQL = SQL::newSelect('field');
        }
        $SQL->addSelect('field_key');
        $SQL->addSelect('field_value');
        $SQL->addSelect('field_type');
        $SQL->addSelect('field_search');
        if (!is_null($bid)) {
            $SQL->addWhereOpr('field_bid', $bid);
        }
        if (!is_null($uid)) {
            $SQL->addWhereOpr('field_uid', $uid);
        }
        if (!is_null($cid)) {
            $SQL->addWhereOpr('field_cid', $cid);
        }
        if (!is_null($eid)) {
            $SQL->addWhereOpr('field_eid', $eid);
        }
        if (!is_null($mid)) {
            $SQL->addWhereOpr('field_mid', $mid);
        }
        if (!is_null($unitId)) {
            $SQL->addWhereOpr('field_unit_id', $unitId);
        }
        $SQL->setOrder('field_sort');
        $q  = $SQL->get(dsn());
        $statement = DB::query($q, 'exec');

        while ($row = DB::next($statement)) {
            $fd = $row['field_key'];
            $field->addField($fd, $row['field_value']);
            $field->setMeta($fd, 'search', $row['field_search'] === 'on');
            $field->setMeta($fd, 'type', $row['field_type']);
        }
        $cacheItem->set($field);
        $this->cacheField->putItem($cacheItem);
        Tpl::injectMediaField($field, force: true);

        return $field;
    }


    /**
     * カスタムフィールドの保存
     *
     * @param 'bid'|'uid'|'cid'|'mid'|'eid'|'unit_id' $type
     * @param ($type is 'unit_id' ? string : int) $id
     * @param Field|null $field
     * @param Field|null $deleteField
     * @param int|null $rvid
     * @param int|null $targetBid 保存先ブログ ID。null / 省略時は現在のブログ（BID）
     *
     * @return bool
     */
    public function save($type, $id, $field = null, $deleteField = null, $rvid = null, $targetBid = null)
    {
        // Why not 引数のデフォルト値に BID を書く: 定数デフォルトはファサードの @method（docblock）に
        // 表現できず「?int $targetBid = null」と乖離する。docblock を信じて null を明示すると
        // field_blog_id = null で INSERT され、NOT NULL 制約違反で DB::query('exec') が false を返し
        // （本番 debug=false では例外化しない）カスタムフィールドが黙って保存されない。
        // null を BID に倒すことで、docblock どおりの呼び出しが安全に動くようにする。
        $targetBid = $targetBid ?? BID;

        // CMS-7676: unit_id は UUID 文字列、それ以外（bid/uid/cid/mid/eid）は数値 ID。
        // UUID に数値比較（$id < 1）を適用すると、先頭が '0' の UUID が
        // 文字列の辞書順比較で "1" 未満と判定され、不正な ID として弾かれてしまう。
        // そのため ID の型に応じて空判定を分岐する。
        $isInvalidId = $type === 'unit_id'
            ? ($id === '')
            : ($id < 1);
        if ($isInvalidId) {
            AcmsLogger::warning('idが空で、フィールドを保存できませんでした', [
                'type' => $type,
                'bid' => $targetBid,
            ]);
            return false;
        }

        $this->deleteFieldCache($type, $id, $rvid);

        $ARCHIVES_DIR_TO = ARCHIVES_DIR;
        $tableName = 'field';
        $asNewVersion = false;

        if (
            1
            && enableRevision()
            && $rvid
            && in_array($type, ['eid', 'unit_id'], true)
        ) {
            $tableName = 'field_rev';
            if (Entry::isNewVersion()) {
                $asNewVersion = true;
            }
        }

        // DELETE 前に、このトランザクションで孤児化するファイルパスを収集する
        // 実際の物理削除は DELETE → INSERT 完了後に行う（参照チェックで自分自身の除外が不要になるため）
        $orphanedPaths = $this->fileGarbageCollector->collectOrphans($tableName, $type, $id, $field, $rvid);

        $SQL = SQL::newDelete($tableName);
        $SQL->addWhereOpr('field_' . $type, $id);
        if ($tableName  === 'field_rev') {
            $SQL->addWhereOpr('field_rev_id', $rvid);
        }
        if ($field && $field->get('updateField') === 'on') {
            $fkey   = [];
            $field->delete('updateField');
            foreach ($field->listFields() as $fd) {
                $fkey[] = $fd;
            }
            $SQL->addWhereIn('field_key', $fkey);
        }
        DB::query($SQL->get(dsn()), 'exec');

        if ($field instanceof Field) {
            $sql = SQL::newBulkInsert($tableName);
            $sql->addColumn('field_key');
            $sql->addColumn('field_value');
            $sql->addColumn('field_type');
            $sql->addColumn('field_sort');
            $sql->addColumn('field_search');
            $sql->addColumn('field_' . $type);
            $sql->addColumn('field_blog_id');
            if ($tableName  === 'field_rev') {
                $sql->addColumn('field_rev_id');
            }
            foreach ($field->listFields() as $fd) {
                // copy revision
                if ($asNewVersion) {
                    if (strpos($fd, '@path')) {
                        $list         = $field->getArray($fd, true);
                        $base         = substr($fd, 0, (-1 * strlen('@path')));
                        $currentLarge = $field->getArray($base . '@largePath', true);
                        $currentTiny  = $field->getArray($base . '@tinyPath', true);
                        $currentSquare = $field->getArray($base . '@squarePath', true);
                        $newPaths        = [];
                        $newLargePaths   = [];
                        $newTinyPaths    = [];
                        $newSquarePaths  = [];
                        $needRebuild = false;
                        foreach ($list as $i => $val) {
                            $path = $val;
                            if (in_array($path, Entry::getUploadedFiles(), true)) {
                                // 今回のリクエストでアップロードされたファイルはコピー不要、そのまま維持
                                $newPaths[]       = $path;
                                $newLargePaths[]  = $currentLarge[$i] ?? '';
                                $newTinyPaths[]   = $currentTiny[$i] ?? '';
                                $newSquarePaths[] = $currentSquare[$i] ?? '';
                                continue;
                            }
                            $needRebuild = true;
                            if (PublicStorage::isFile(ARCHIVES_DIR . $path)) {
                                $info       = pathinfo($path);
                                $dirname    = ($info['dirname'] ?? '') === '' ? '' : $info['dirname'] . '/';
                                PublicStorage::makeDirectory($ARCHIVES_DIR_TO . $dirname);
                                $ext        = ($info['extension'] ?? '') === '' ? '' : '.' . $info['extension'];
                                $newPath    = $dirname . uniqueString() . $ext;

                                $path       = ARCHIVES_DIR . $path;
                                $largePath  = otherSizeImagePath($path, 'large');
                                $tinyPath   = otherSizeImagePath($path, 'tiny');
                                $squarePath = otherSizeImagePath($path, 'square');

                                $newLargePath   = otherSizeImagePath($newPath, 'large');
                                $newTinyPath    = otherSizeImagePath($newPath, 'tiny');
                                $newSquarePath  = otherSizeImagePath($newPath, 'square');

                                PublicStorage::copy($path, $ARCHIVES_DIR_TO . $newPath);
                                PublicStorage::copy($largePath, $ARCHIVES_DIR_TO . $newLargePath);
                                PublicStorage::copy($tinyPath, $ARCHIVES_DIR_TO . $newTinyPath);
                                PublicStorage::copy($squarePath, $ARCHIVES_DIR_TO . $newSquarePath);

                                if (!PublicStorage::isReadable($newLargePath)) {
                                    $newLargePath = '';
                                }
                                if (!PublicStorage::isReadable($newTinyPath)) {
                                    $newTinyPath = '';
                                }
                                if (!PublicStorage::isReadable($newSquarePath)) {
                                    $newSquarePath = '';
                                }
                                $newPaths[]       = $newPath;
                                $newLargePaths[]  = $newLargePath;
                                $newTinyPaths[]   = $newTinyPath;
                                $newSquarePaths[] = $newSquarePath;
                            } else {
                                $newPaths[]       = '';
                                $newLargePaths[]  = '';
                                $newTinyPaths[]   = '';
                                $newSquarePaths[] = '';
                            }
                        }
                        if ($needRebuild) {
                            // 既存パスをコピーしたときだけ、@path と関連バリエーションをまとめて差し替える
                            $field->delete($fd);
                            $field->delete($base . '@largePath');
                            $field->delete($base . '@tinyPath');
                            $field->delete($base . '@squarePath');
                            foreach ($newPaths as $v) {
                                $field->add($fd, $v);
                            }
                            foreach ($newLargePaths as $v) {
                                $field->add($base . '@largePath', $v);
                            }
                            foreach ($newTinyPaths as $v) {
                                $field->add($base . '@tinyPath', $v);
                            }
                            foreach ($newSquarePaths as $v) {
                                $field->add($base . '@squarePath', $v);
                            }
                        }
                    }
                }
                foreach ($field->getArray($fd, true) as $i => $val) {
                    $fieldTypeValue = null;
                    if (preg_match('/@(html|media|title)$/', $fd, $match)) {
                        $fieldTypeValue = $match[1];
                    }
                    if ($fieldType = $field->getMeta($fd, 'type')) {
                        $fieldTypeValue = $fieldType;
                    }
                    $data = [
                        'field_key' => $fd,
                        'field_value' => $val,
                        'field_type' => $fieldTypeValue,
                        'field_sort' => $i + 1,
                        'field_search' => $field->getMeta($fd, 'search') ? 'on' : 'off',
                        'field_' . $type => $id,
                        'field_blog_id' => $targetBid,
                    ];
                    if ($tableName  === 'field_rev') {
                        $data['field_rev_id'] = $rvid;
                    }
                    $sql->addInsert($data);
                }
            }
            if ($sql->hasData()) {
                DB::query($sql->get(dsn()), 'exec');
            }
        }

        // DELETE → INSERT 完了後に、どこからも参照されなくなったファイルを物理削除する
        // 行削除由来（collectOrphans）と削除チェック・差し替え由来（collectMarked）を合算して一括処理する
        $allCandidates = array_merge($orphanedPaths, $this->fileGarbageCollector->collectMarked($field));
        if ($allCandidates !== []) {
            $this->fileGarbageCollector->removeUnreferenced($allCandidates);
        }

        return true;
    }
}
