<?php
namespace Misuzu\Profile;

use InvalidArgumentException;
use RuntimeException;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Misuzu\Users\UserInfo;

class ProfileFields {
    private DbStatementCache $cache;

    public function __construct(IDbConnection $dbConn) {
        $this->cache = new DbStatementCache($dbConn);
    }

    public function getFields(?array $fieldValueInfos = null): array {
        $hasFieldValueInfos = $fieldValueInfos !== null;
        if($hasFieldValueInfos && empty($fieldValueInfos))
            return [];

        $query = 'SELECT field_id, field_order, field_key, field_title, field_regex FROM msz_profile_fields';
        if($hasFieldValueInfos)
            $query .= sprintf(' WHERE field_id IN (%s)', DbTools::prepareListString($fieldValueInfos));
        $query .= ' ORDER BY field_order ASC';

        $stmt = $this->cache->get($query);
        $args = 0;

        if($hasFieldValueInfos)
            foreach($fieldValueInfos as $fieldValueInfo) {
                if(!($fieldValueInfo instanceof ProfileFieldValueInfo))
                    throw new InvalidArgumentException('All values in $fieldValueInfos must be of ProfileFieldValueInfo type.');
                $stmt->addParameter(++$args, $fieldValueInfo->getFieldId());
            }

        $stmt->execute();
        $result = $stmt->getResult();
        $fields = [];

        while($result->next())
            $fields[] = new ProfileFieldInfo($result);

        return $fields;
    }

    public function getField(string $fieldId): ProfileFieldInfo {
        $stmt = $this->cache->get('SELECT field_id, field_order, field_key, field_title, field_regex FROM msz_profile_fields WHERE field_id = ?');
        $stmt->addParameter(1, $fieldId);
        $stmt->execute();

        $result = $stmt->getResult();
        if(!$result->next())
            throw new RuntimeException('No field found with the provided field id.');

        return new ProfileFieldInfo($result);
    }

    public function getFieldFormats(
        ?array $fieldInfos = null,
        ?array $fieldValueInfos = null
    ): array {
        $hasFieldInfos = $fieldInfos !== null;
        $hasFieldValueInfos = $fieldValueInfos !== null;

        if($hasFieldInfos && empty($fieldInfos))
            return [];
        if($hasFieldValueInfos && empty($fieldValueInfos))
            return [];

        $args = 0;
        $query = 'SELECT format_id, field_id, format_regex, format_link, format_display FROM msz_profile_fields_formats';

        if($hasFieldInfos) {
            ++$args;
            $query .= sprintf(' WHERE field_id IN (%s)', DbTools::prepareListString($fieldInfos));
        }
        if($hasFieldValueInfos)
            $query .= sprintf(' %s format_id IN (%s)',
                (++$args > 1 ? 'AND' : 'WHERE'),
                DbTools::prepareListString($fieldValueInfos)
            );

        $stmt = $this->cache->get($query);
        $args = 0;

        if($hasFieldInfos)
            foreach($fieldInfos as $fieldInfo) {
                if(!($fieldInfo instanceof ProfileFieldInfo))
                    throw new InvalidArgumentException('All values in $fieldInfos must be of ProfileFieldInfo type.');
                $stmt->addParameter(++$args, $fieldInfo->getId());
            }

        if($hasFieldValueInfos)
            foreach($fieldValueInfos as $fieldValueInfo) {
                if(!($fieldValueInfo instanceof ProfileFieldValueInfo))
                    throw new InvalidArgumentException('All values in $fieldValueInfos must be of ProfileFieldValueInfo type.');
                $stmt->addParameter(++$args, $fieldValueInfo->getFormatId());
            }

        $stmt->execute();
        $result = $stmt->getResult();
        $formats = [];

        while($result->next())
            $formats[] = new ProfileFieldFormatInfo($result);

        return $formats;
    }

    public function getFieldFormat(string $formatId): ProfileFieldFormatInfo {
        $stmt = $this->cache->get('SELECT format_id, field_id, format_regex, format_link, format_display FROM msz_profile_fields_formats WHERE format_id = ?');
        $stmt->addParameter(1, $formatId);
        $stmt->execute();

        $result = $stmt->getResult();
        if(!$result->next())
            throw new RuntimeException('No format found with the provided format id.');

        return new ProfileFieldFormatInfo($result);
    }

    public function selectFieldFormat(
        ProfileFieldInfo|string $fieldInfo,
        string $value
    ): ProfileFieldFormatInfo {
        if($fieldInfo instanceof ProfileFieldInfo)
            $fieldInfo = $fieldInfo->getId();

        $stmt = $this->cache->get('SELECT format_id, field_id, format_regex, format_link, format_display FROM msz_profile_fields_formats WHERE field_id = ? AND (format_regex IS NULL OR ? REGEXP format_regex) ORDER BY format_regex IS NULL ASC');
        $stmt->addParameter(1, $fieldInfo);
        $stmt->addParameter(2, $value);
        $stmt->execute();

        $result = $stmt->getResult();
        if(!$result->next())
            throw new RuntimeException('Could not determine an appropriate format for this field (missing default formatting)');

        return new ProfileFieldFormatInfo($result);
    }

    public function getFieldValues(UserInfo|string $userInfo): array {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        // i don't really want to bother with the join for the ordering so i'll just do that somewhere in PHP for now
        // will probably add the ability for people to order them in whatever way they want, as well as visibility controls
        $stmt = $this->cache->get('SELECT field_id, user_id, format_id, field_value FROM msz_profile_fields_values WHERE user_id = ?');
        $stmt->addParameter(1, $userInfo);
        $stmt->execute();

        $result = $stmt->getResult();
        $values = [];

        while($result->next())
            $values[] = new ProfileFieldValueInfo($result);

        return $values;
    }

    public function getFieldValue(
        ProfileFieldInfo|string $fieldInfo,
        UserInfo|string $userInfo
    ): ProfileFieldValueInfo {
        if($fieldInfo instanceof ProfileFieldInfo)
            $fieldInfo = $fieldInfo->getId();
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $stmt = $this->cache->get('SELECT field_id, user_id, format_id, field_value FROM msz_profile_fields_values WHERE field_id = ? AND user_id = ?');
        $stmt->addParameter(1, $fieldInfo);
        $stmt->addParameter(2, $userInfo);
        $stmt->execute();

        $result = $stmt->getResult();

        if(!$result->next())
            throw new RuntimeException('No value for this field and user combination found.');

        return new ProfileFieldValueInfo($result);
    }

    public function setFieldValues(
        UserInfo|string $userInfo,
        ProfileFieldInfo|string|array $fieldInfos,
        string|array $values
    ): void {
        if(empty($fieldInfos))
            return;

        if(!is_array($fieldInfos)) {
            if(is_array($values))
                throw new InvalidArgumentException('If $fieldInfos is not an array, $values may not be either');
            $fieldInfos = [$fieldInfos];
            $values = [$values];
        } elseif(!is_array($values))
            throw new InvalidArgumentException('If $fieldInfos is an array, $values must be as well.');

        $fieldsCount = count($fieldInfos);
        if($fieldsCount !== count($values))
            throw new InvalidArgumentException('$fieldsInfos and $values have the same amount of values and be in the same order.');

        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $rows = [];

        foreach($fieldInfos as $key => $fieldInfo) {
            if(is_string($fieldInfo))
                $fieldInfo = $this->getField($fieldInfo);
            elseif(!($fieldInfo instanceof ProfileFieldInfo))
                throw new InvalidArgumentException('Entries of $fieldInfos must either be field IDs or instances of ProfileFieldInfo.');

            $value = $fieldInfo->matchValue($values[$key]);
            if($value === false)
                throw new InvalidArgumentException('One of the values in $values is not correct formatted.');

            $rows[] = [
                $fieldInfo->getId(),
                $this->selectFieldFormat($fieldInfo, $value)->getId(),
                $value,
            ];
        }

        $args = 0;
        $stmt = $this->cache->get(
            'REPLACE INTO msz_profile_fields_values (field_id, user_id, format_id, field_value) VALUES '
                . DbTools::prepareListString($rows, '(?, ?, ?, ?)')
        );

        foreach($rows as $row) {
            $stmt->addParameter(++$args, $row[0]);
            $stmt->addParameter(++$args, $userInfo);
            $stmt->addParameter(++$args, $row[1]);
            $stmt->addParameter(++$args, $row[2]);
        }

        $stmt->execute();
    }

    public function removeFieldValues(
        UserInfo|string $userInfo,
        ProfileFieldInfo|string|array $fieldInfos
    ): void {
        if(empty($fieldInfos))
            return;
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        if(!is_array($fieldInfos))
            $fieldInfos = [$fieldInfos];

        foreach($fieldInfos as $key => $value) {
            if($value instanceof ProfileFieldInfo)
                $fieldInfos[$key] = $value->getId();
            elseif(is_string($value))
                throw new InvalidArgumentException('$fieldInfos array may only contain string IDs or instances of ProfileFieldInfo');
        }

        $args = 0;
        $stmt = $this->cache->get(sprintf(
            'DELETE FROM msz_profile_fields_values WHERE user_id = ? AND field_id IN (%s)',
            DbTools::prepareListString($fieldInfos)
        ));
        $stmt->addParameter(++$args, $userInfo);
        foreach($fieldInfos as $value)
            $stmt->addParameter(++$args, $value);
        $stmt->execute();
    }
}
