<?php

namespace Acms\Services\Form;

use SQL;
use DB;
use Field;
use ACMS_Filter;

/**
 * フォームの永続化を担う実装。
 *
 * SQL・テーブル/カラム名・シーケンス操作に加えて、
 * 生レコード→ドメイン配列の変換（toArray）と、入力 Field→保存カラムの変換（toAttributes / form_data のシリアライズ）を
 * private メソッドとして内部に閉じ込める。
 */
class Repository implements RepositoryInterface
{
    /**
     * @inheritDoc
     */
    public function findById(int $formId): ?array
    {
        $row = $this->findRawById($formId);

        return $row === null ? null : $this->toArray($row);
    }

    /**
     * @inheritDoc
     */
    public function findByCodeInTree(string $code, int $blogId): ?array
    {
        if ($code === '') {
            return null;
        }
        $sql = SQL::newSelect('form');
        $sql->addWhereOpr('form_code', $code);
        $sql->addLeftJoin('blog', 'blog_id', 'form_blog_id');
        ACMS_Filter::blogTree($sql, $blogId, 'ancestor-or-self');

        $where = SQL::newWhere();
        $where->addWhereOpr('form_blog_id', $blogId, '=', 'OR');
        $where->addWhereOpr('form_scope', 'global', '=', 'OR');
        $sql->addWhere($where);
        $row = DB::query($sql->get(dsn()), 'row');

        return $row ? $this->toArray($row) : null;
    }

    /**
     * @inheritDoc
     */
    public function findRawById(int $formId): ?array
    {
        if ($formId < 1) {
            return null;
        }
        $sql = SQL::newSelect('form');
        $sql->addWhereOpr('form_id', $formId);
        $row = DB::query($sql->get(dsn()), 'row');

        return is_array($row) ? $row : null;
    }

    /**
     * @inheritDoc
     */
    public function existsCodeInTree(string $code, ?int $excludeFormId, string $scope, int $blogId): bool
    {
        $sql = SQL::newSelect('form');
        $sql->setSelect('form_id');
        $sql->addLeftJoin('blog', 'blog_id', 'form_blog_id');

        if ($scope === 'local') {
            ACMS_Filter::blogTree($sql, $blogId, 'ancestor-or-self');
            $where = SQL::newWhere();
            $where->addWhereOpr('form_blog_id', $blogId, '=', 'OR');
            $where->addWhereOpr('form_scope', 'global', '=', 'OR');
            $sql->addWhere($where);
        } else {
            ACMS_Filter::blogTree($sql, $blogId, 'ancestor-or-descendant-or-self');
        }

        $sql->addWhereOpr('form_code', $code);
        if (is_int($excludeFormId)) {
            $sql->addWhereOpr('form_id', $excludeFormId, '<>');
        }
        $sql->setLimit(1);

        return (bool)DB::query($sql->get(dsn()), 'one');
    }

    /**
     * @inheritDoc
     */
    public function findIdByCodeInBlog(string $code, int $blogId): ?int
    {
        $sql = SQL::newSelect('form');
        $sql->setSelect('form_id');
        $sql->addWhereOpr('form_code', $code);
        $sql->addWhereOpr('form_blog_id', $blogId);
        $sql->setLimit(1);
        $id = DB::query($sql->get(dsn()), 'one');

        return $id ? (int)$id : null;
    }

    /**
     * @inheritDoc
     */
    public function existsMailInLog(int $formId, string $mail, int $blogId): bool
    {
        $quoted = preg_quote($mail);
        $regex = "^{$quoted}$|[^a-z]{$quoted}\s?[>]?[^a-z]";

        $sql = SQL::newSelect('log_form');
        $sql->addSelect('log_form_mail_to');
        $sql->addWhereOpr('log_form_mail_to', $regex, 'REGEXP');
        $sql->addWhereOpr('log_form_form_id', $formId);
        $sql->addWhereOpr('log_form_blog_id', $blogId);

        return (bool)DB::query($sql->get(dsn()), 'one');
    }

    /**
     * @inheritDoc
     */
    public function insert(int $blogId, Field|array $form): int
    {
        $attributes = $this->normalizeAttributes($form);

        $formId = (int)DB::query(SQL::nextval('form_id', dsn()), 'seq');

        $sql = SQL::newInsert('form');
        $sql->addInsert('form_id', $formId);
        $sql->addInsert('form_code', $attributes['code'] ?? '');
        $sql->addInsert('form_name', $attributes['name'] ?? '');
        $sql->addInsert('form_scope', $attributes['scope'] ?? 'local');
        $sql->addInsert('form_log', $attributes['log'] ?? '1');
        $sql->addInsert('form_data', $attributes['data'] ?? '');
        $sql->addInsert('form_current_serial', 0);
        $sql->addInsert('form_blog_id', $blogId);
        DB::query($sql->get(dsn()), 'exec');

        return $formId;
    }

    /**
     * @inheritDoc
     */
    public function update(int $formId, int $blogId, Field|array $form): void
    {
        $attributes = $this->normalizeAttributes($form);

        // 属性キー → 更新カラムの対応。$attributes に含まれるキーのカラムだけを更新する（部分更新）。
        // Field 由来（toAttributes）は全キーを持つため全カラム更新、連想配列は渡したキーだけ更新になる。
        $columns = [
            'code' => 'form_code',
            'name' => 'form_name',
            'scope' => 'form_scope',
            'log' => 'form_log',
            'data' => 'form_data',
        ];

        $sql = SQL::newUpdate('form');
        $hasUpdate = false;
        foreach ($columns as $key => $column) {
            if (array_key_exists($key, $attributes)) {
                $sql->addUpdate($column, $attributes[$key]);
                $hasUpdate = true;
            }
        }
        if (!$hasUpdate) {
            return; // 更新対象カラムが無ければ何もしない（不正な UPDATE を発行しない）
        }
        $sql->addWhereOpr('form_id', $formId);
        $sql->addWhereOpr('form_blog_id', $blogId);
        DB::query($sql->get(dsn()), 'exec');
    }

    /**
     * @inheritDoc
     */
    public function duplicate(int $sourceFormId, int $blogId, string $codeSuffix, string $nameSuffix): int
    {
        $newId = (int)DB::query(SQL::nextval('form_id', dsn()), 'seq');

        $sql = SQL::newSelect('form');
        $sql->addWhereOpr('form_id', $sourceFormId);
        $sql->addWhereOpr('form_blog_id', $blogId);
        /** @var array<string, mixed> $base */
        $base = DB::query($sql->get(dsn()), 'row');

        $base['form_id'] = $newId;
        $base['form_code'] .= $codeSuffix . $newId;
        $base['form_name'] .= $nameSuffix;
        $base['form_current_serial'] = 0;

        $sql = SQL::newInsert('form');
        foreach ($base as $key => $val) {
            $sql->addInsert($key, $val);
        }
        DB::query($sql->get(dsn()), 'exec');

        return $newId;
    }

    /**
     * @inheritDoc
     */
    public function delete(int $formId): void
    {
        // フォーム本体と、それに紐づく投稿ログをまとめて削除する（フォーム集約の削除）。
        // form_id は主キーのため、これだけで対象は一意に定まる。
        $sql = SQL::newDelete('form');
        $sql->addWhereOpr('form_id', $formId);
        DB::query($sql->get(dsn()), 'exec');

        $sql = SQL::newDelete('log_form');
        $sql->addWhereOpr('log_form_form_id', $formId);
        DB::query($sql->get(dsn()), 'exec');
    }

    /**
     * @inheritDoc
     */
    public function optimizeSequence(): void
    {
        DB::query(SQL::optimizeSeq('form_id', dsn()), 'seq');
    }

    /**
     * フォームの生レコードを構造化（ドメイン）配列へ変換する
     *
     * @param array<string, mixed> $row
     * @return array{id:int, bid:int, code:string, name:string, scope:string, log:'0'|'1', data:\Field}
     */
    private function toArray(array $row): array
    {
        return [
            'id'    => (int)$row['form_id'],
            'bid'   => (int)$row['form_blog_id'],
            'code'  => $row['form_code'],
            'name'  => $row['form_name'],
            'scope' => $row['form_scope'],
            'log'   => (string)$row['form_log'] === '1' ? '1' : '0',
            'data'  => acmsDangerUnserialize($row['form_data']),
        ];
    }

    /**
     * insert / update の引数（Field または保存属性の連想配列）を保存属性へ正規化する
     *
     * @param \Field|array{code?:string, name?:string, scope?:string, log?:'0'|'1', data?:string} $form
     * @return array{code?:string, name?:string, scope?:string, log?:'0'|'1', data?:string}
     */
    private function normalizeAttributes(Field|array $form): array
    {
        return $form instanceof Field ? $this->toAttributes($form) : $form;
    }

    /**
     * 入力 Field を form テーブルの保存属性へ変換する
     *
     * @param \Field $form code/name/scope/log と各フィールド設定を持つ入力
     * @return array{code:string, name:string, scope:string, log:'0'|'1', data:string}
     */
    private function toAttributes(Field $form): array
    {
        return [
            'code' => $form->get('code'),
            'name' => $form->get('name'),
            'scope' => $form->get('scope', 'local'),
            'log' => in_array($form->get('log', '1'), ['0', '1'], true) ? $form->get('log', '1') : '1',
            'data' => $this->serializeData($form),
        ];
    }

    /**
     * 入力 Field から form_data 保存用のシリアライズ文字列を生成する
     *
     * @param \Field $form
     * @return string
     */
    private function serializeData(Field $form): string
    {
        $data = new Field($form, true);
        $data->delete('code');
        $data->delete('name');
        $data->delete('log');

        return acmsSerialize($data);
    }
}
