<?php

declare(strict_types=1);

namespace FecaMailshots\Application;

use FecaMailshots\Repository\AttachmentRepository;
use FecaMailshots\Repository\LastRunRepository;
use FecaMailshots\Repository\MailshotQueryRepository;
use FecaMailshots\Repository\MailshotRepository;

final class MailshotService
{
    private MailshotRepository $mailshots;
    private MailshotQueryRepository $queries;
    private AttachmentRepository $attachments;
    private LastRunRepository $lastRun;
    private DataSourceService $dataSources;

    public function __construct(
        MailshotRepository $mailshots,
        MailshotQueryRepository $queries,
        AttachmentRepository $attachments,
        LastRunRepository $lastRun,
        DataSourceService $dataSources
    ) {
        $this->mailshots = $mailshots;
        $this->queries = $queries;
        $this->attachments = $attachments;
        $this->lastRun = $lastRun;
        $this->dataSources = $dataSources;
    }

    /** @return list<array<string, mixed>> */
    public function list(): array
    {
        return $this->mailshots->all();
    }

    /** @return list<string> */
    public function dataSourceNames(): array
    {
        $rows = $this->queries->all();
        return array_map(static fn(array $r): string => (string) $r['name'], $rows);
    }

    /** @return list<string> */
    public function attachmentNames(): array
    {
        return $this->attachments->names();
    }

    /** @return array<string, mixed> */
    public function save(?int $id, array $payload): array
    {
        $purpose = trim((string) ($payload['Purpose'] ?? ''));
        $dataSource = trim((string) ($payload['DataSource'] ?? ''));
        $recipientEmailField = trim((string) ($payload['RecipientEmailField'] ?? ''));
        $subject = trim((string) ($payload['Subject'] ?? ''));
        $message = trim((string) ($payload['Message'] ?? ''));

        $errors = [];
        if ($dataSource === '') {
            $errors[] = 'DataSource is required.';
        } elseif ($this->queries->findByName($dataSource) === null) {
            $errors[] = 'DataSource must match an existing mailshot data source.';
        }
        if ($subject === '') {
            $errors[] = 'Subject is required.';
        }
        if ($recipientEmailField !== '' && $dataSource !== '') {
            $tokens = $this->tokenInsertionData($dataSource);
            $fields = [];
            foreach (($tokens['tokens'] ?? []) as $token) {
                if (is_array($token) && isset($token['field'])) {
                    $fields[] = (string) $token['field'];
                }
            }
            if ($fields !== [] && !in_array($recipientEmailField, $fields, true)) {
                $errors[] = 'RecipientEmailField must be one of the selected DataSource fields.';
            }
        }

        $attachmentNames = $this->normalizeAttachmentNames($payload['AttachmentNames'] ?? null);

        if ($errors !== []) {
            return ['ok' => false, 'errors' => $errors];
        }

        $row = [
            'Purpose' => $purpose,
            'DataSource' => $dataSource,
            'CC' => trim((string) ($payload['CC'] ?? '')),
            'BCC' => trim((string) ($payload['BCC'] ?? '')),
            'Subject' => $subject,
            'Message' => $message,
            'PDFAttachment' => (string) ($payload['PDFAttachment'] ?? ''),
            'AttachmentNames' => json_encode($attachmentNames, JSON_UNESCAPED_SLASHES),
            'PDFFilenameDerivedFrom' => trim((string) ($payload['PDFFilenameDerivedFrom'] ?? '')),
            'ReplyTo' => trim((string) ($payload['ReplyTo'] ?? '')),
            'RecipientEmailField' => $recipientEmailField,
        ];

        if ($id === null) {
            $id = $this->mailshots->create($row);
        } else {
            $this->mailshots->update($id, $row);
        }

        return ['ok' => true, 'id' => $id];
    }

    public function delete(int $id): void
    {
        $this->mailshots->delete($id);
    }

    /** @return array<string, mixed> */
    public function duplicate(int $id): array
    {
        $source = $this->mailshots->find($id);
        if ($source === null) {
            return ['ok' => false, 'errors' => ['Mailshot not found.']];
        }

        $row = [
            'Purpose' => $this->duplicatePurpose((string) ($source['Purpose'] ?? ''), $id),
            'DataSource' => (string) ($source['DataSource'] ?? ''),
            'CC' => (string) ($source['CC'] ?? ''),
            'BCC' => (string) ($source['BCC'] ?? ''),
            'Subject' => (string) ($source['Subject'] ?? ''),
            'Message' => (string) ($source['Message'] ?? ''),
            'PDFAttachment' => (string) ($source['PDFAttachment'] ?? ''),
            'AttachmentNames' => (string) ($source['AttachmentNames'] ?? '[]'),
            'PDFFilenameDerivedFrom' => (string) ($source['PDFFilenameDerivedFrom'] ?? ''),
            'ReplyTo' => (string) ($source['ReplyTo'] ?? ''),
            'RecipientEmailField' => (string) ($source['RecipientEmailField'] ?? ''),
        ];

        $newId = $this->mailshots->create($row);
        return ['ok' => true, 'id' => $newId];
    }

    /** @return list<array<string, mixed>> */
    public function lastRun(int $mailshotId): array
    {
        return $this->lastRun->listForMailshot($mailshotId);
    }

    public function clearLastRun(int $mailshotId): void
    {
        $this->lastRun->clearForMailshot($mailshotId);
    }

    private function duplicatePurpose(string $purpose, int $sourceId): string
    {
        $base = trim($purpose);
        if ($base === '') {
            $base = 'Mailshot #' . $sourceId;
        }

        $existing = [];
        foreach ($this->mailshots->all() as $row) {
            $existing[strtolower(trim((string) ($row['Purpose'] ?? '')))] = true;
        }

        $prefix = 'Copy of ';
        $candidate = $this->truncatePurpose($prefix . $base);
        if (!isset($existing[strtolower($candidate)])) {
            return $candidate;
        }

        for ($i = 2; $i < 1000; $i++) {
            $suffix = ' (' . $i . ')';
            $candidate = $this->truncatePurpose($prefix . $base, $suffix);
            if (!isset($existing[strtolower($candidate)])) {
                return $candidate;
            }
        }

        return $this->truncatePurpose($prefix . $base, ' (' . time() . ')');
    }

    private function truncatePurpose(string $value, string $suffix = ''): string
    {
        $max = 100;
        $suffixLength = strlen($suffix);
        $baseMax = max(1, $max - $suffixLength);
        $base = substr($value, 0, $baseMax);
        return rtrim($base) . $suffix;
    }

    /** @return array{tokens:list<array<string, string>>, errors:list<string>} */
    public function tokenInsertionData(string $dataSourceName): array
    {
        $query = $this->queries->findByName($dataSourceName);
        if ($query === null) {
            return ['tokens' => [], 'errors' => ['Unknown data source.']];
        }

        $dsl = trim((string) ($query['dsl_text'] ?? ''));
        if ($dsl === '') {
            return ['tokens' => [], 'errors' => ['Data source has no DSL text.']];
        }

        $validation = $this->dataSources->validateDsl($dsl);
        if (($validation['errors'] ?? []) !== []) {
            return ['tokens' => [], 'errors' => $validation['errors']];
        }

        $tokens = [];
        $seen = [];
        foreach (($validation['expected_fields'] ?? []) as $qualifiedField) {
            $tokenName = $this->canonicalTokenName((string) $qualifiedField);
            if ($tokenName === '' || isset($seen[$tokenName])) {
                continue;
            }
            $seen[$tokenName] = true;
            $tokens[] = [
                'field' => (string) $qualifiedField,
                'token_name' => $tokenName,
                'token' => '{{ ' . $tokenName . ' }}',
            ];
        }

        return ['tokens' => $tokens, 'errors' => []];
    }

    /** @return list<string> */
    private function normalizeAttachmentNames($raw): array
    {
        $decoded = [];
        if (is_string($raw) && trim($raw) !== '') {
            $fromJson = json_decode($raw, true);
            if (is_array($fromJson)) {
                $decoded = $fromJson;
            }
        } elseif (is_array($raw)) {
            $decoded = $raw;
        }

        $result = [];
        foreach ($decoded as $item) {
            $v = trim((string) $item);
            if ($v !== '') {
                $result[] = $v;
            }
        }
        return array_values(array_unique($result));
    }

    private function canonicalTokenName(string $qualifiedField): string
    {
        $name = strtolower($qualifiedField);
        $name = str_replace('.', '_', $name);
        $name = preg_replace('/[^a-z0-9_]+/', '_', $name) ?? '';
        $name = preg_replace('/_+/', '_', $name) ?? '';
        return trim($name, '_');
    }
}
