<?php
declare(strict_types=1);

namespace App\Community;

use App\Settings\SettingsRepository;
use App\Support\TranslatableException;
use InvalidArgumentException;

final class PostingPolicyService
{
    /** @var array<string, int|string> */
    private array $rules;

    /** @param array<string, int|string> $rules */
    public function __construct(array $rules = [])
    {
        $this->rules = array_replace(self::defaults(), $rules);
        $this->normalizeRules();
    }

    public static function fromSettings(SettingsRepository $settings): self
    {
        $rules = self::defaults();
        foreach ($rules as $key => $default) {
            $value = $settings->get('quality.' . $key, $default);
            $rules[$key] = is_int($default) ? (int) $value : (string) $value;
        }
        return new self($rules);
    }

    /** @return array<string, int|string> */
    public static function defaults(): array
    {
        return [
            'question_title_min' => 10,
            'question_title_max' => 180,
            'question_body_min' => 20,
            'question_body_max' => 20000,
            'answer_min' => 10,
            'answer_max' => 20000,
            'comment_min' => 3,
            'comment_max' => 600,
            'tags_min' => 0,
            'tags_max' => 5,
            'question_rate_hour' => 5,
            'answer_rate_hour' => 20,
            'comment_rate_hour' => 30,
            'comment_rate_minute' => 5,
            'tag_create_rate_hour' => 10,
            'comment_max_links' => 2,
            'duplicate_content_minutes' => 10,
            'banned_words' => '',
            'banned_domains' => '',
        ];
    }

    private function normalizeRules(): void
    {
        $this->rules['tags_max'] = max(1, min(5, (int) ($this->rules['tags_max'] ?? 5)));
        $this->rules['tags_min'] = max(0, min((int) $this->rules['tags_max'], (int) ($this->rules['tags_min'] ?? 0)));

        foreach ([
            ['question_title_min', 'question_title_max'],
            ['question_body_min', 'question_body_max'],
            ['answer_min', 'answer_max'],
            ['comment_min', 'comment_max'],
        ] as [$minKey, $maxKey]) {
            $min = max(0, (int) ($this->rules[$minKey] ?? 0));
            $max = max(0, (int) ($this->rules[$maxKey] ?? 0));
            if ($max > 0 && $max < $min) {
                $max = $min;
            }
            $this->rules[$minKey] = $min;
            $this->rules[$maxKey] = $max;
        }

        foreach (['question_rate_hour', 'answer_rate_hour', 'comment_rate_hour', 'comment_rate_minute', 'tag_create_rate_hour'] as $key) {
            $this->rules[$key] = max(1, (int) ($this->rules[$key] ?? 1));
        }
        $this->rules['comment_max_links'] = max(0, min(20, (int) ($this->rules['comment_max_links'] ?? 2)));
        $this->rules['duplicate_content_minutes'] = max(0, min(1440, (int) ($this->rules['duplicate_content_minutes'] ?? 10)));
    }

    public function validateQuestion(string $title, string $body, int $tagCount): void
    {
        $this->assertLength($title, 'question_title_min', 'question_title_max', 'question_title');
        $this->assertLength($body, 'question_body_min', 'question_body_max', 'question_body');
        if ($tagCount < $this->int('tags_min') || $tagCount > $this->int('tags_max')) {
            throw new TranslatableException(
                'content.tags_range',
                ['min' => $this->int('tags_min'), 'max' => $this->int('tags_max')],
                sprintf('A question must use between %d and %d tags.', $this->int('tags_min'), $this->int('tags_max'))
            );
        }
        $this->assertContentAllowed($title . "\n" . $body);
    }

    public function validateAnswer(string $body): void
    {
        $this->assertLength($body, 'answer_min', 'answer_max', 'answer');
        $this->assertContentAllowed($body);
    }

    public function validateComment(string $body): void
    {
        $this->assertLength($body, 'comment_min', 'comment_max', 'comment');
        $this->assertContentAllowed($body);
        if ($this->countLinks($body) > $this->int('comment_max_links')) {
            throw new TranslatableException(
                'content.comment_max_links',
                ['count' => $this->int('comment_max_links')],
                sprintf('A comment may contain at most %d links.', $this->int('comment_max_links'))
            );
        }
    }

    public function validateTagName(string $name): void
    {
        $name = trim($name);
        $length = $this->unicodeLength($name);
        if ($length < 2 || $length > 64) {
            throw new TranslatableException('content.tag_name_length', [], 'Tag names must contain between 2 and 64 characters.');
        }
        $this->assertContentAllowed($name);
    }

    public function int(string $key): int
    {
        return max(0, (int) ($this->rules[$key] ?? 0));
    }

    public function string(string $key): string
    {
        return trim((string) ($this->rules[$key] ?? ''));
    }

    /** @return array<string,int|string> */
    public function rules(): array
    {
        return $this->rules;
    }

    private function assertLength(string $value, string $minKey, string $maxKey, string $label): void
    {
        $length = $this->unicodeLength(trim($value));
        $min = $this->int($minKey);
        $max = $this->int($maxKey);
        if ($length < $min) {
            throw new TranslatableException(
                'content.min_length_' . $label,
                ['count' => $min],
                sprintf('%s must contain at least %d characters.', $label, $min)
            );
        }
        if ($max > 0 && $length > $max) {
            throw new TranslatableException(
                'content.max_length_' . $label,
                ['count' => $max],
                sprintf('%s may contain at most %d characters.', $label, $max)
            );
        }
    }

    private function assertContentAllowed(string $value): void
    {
        $normalized = $this->lower($value);
        foreach ($this->list('banned_words') as $word) {
            if ($word !== '' && str_contains($normalized, $this->lower($word))) {
                throw new TranslatableException('content.banned_word', [], 'The content contains a blocked word or phrase.');
            }
        }
        foreach ($this->list('banned_domains') as $domain) {
            $domain = $this->lower(preg_replace('~^https?://~i', '', trim($domain)) ?? '');
            if ($domain !== '' && str_contains($normalized, $domain)) {
                throw new TranslatableException('content.banned_domain', [], 'The content contains a blocked domain.');
            }
        }
    }

    /** @return array<int, string> */
    private function list(string $key): array
    {
        $raw = $this->string($key);
        if ($raw === '') return [];
        $parts = preg_split('/[\r\n,;]+/u', $raw) ?: [];
        return array_values(array_unique(array_filter(array_map('trim', $parts), static fn (string $value): bool => $value !== '')));
    }

    private function countLinks(string $body): int
    {
        preg_match_all('~(?:https?://|www\.)[^\s<]+~iu', $body, $matches);
        return count($matches[0] ?? []);
    }

    private function unicodeLength(string $value): int
    {
        if (function_exists('mb_strlen')) return mb_strlen($value, 'UTF-8');
        $count = preg_match_all('/./us', $value, $matches);
        return is_int($count) && $count >= 0 ? $count : strlen($value);
    }

    private function lower(string $value): string
    {
        return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
    }
}
