<?php

declare(strict_types=1);

namespace FecaMailshots\Application;

use FecaMailshots\Domain\DslParser;
use FecaMailshots\Infrastructure\DatabaseRouter;
use FecaMailshots\Repository\MailshotRepository;
use FecaMailshots\Repository\MailshotQueryRepository;
use PDO;

final class DataSourceService
{
    private const NAME_MAX_LENGTH = 100;

    private MailshotQueryRepository $queries;
    private DatabaseRouter $router;
    private DslParser $parser;
    private DslValidator $validator;
    private DslCompiler $compiler;
    private SourceMetadataProvider $metadata;
    private MailshotRepository $mailshots;

    public function __construct(
        MailshotQueryRepository $queries,
        DatabaseRouter $router,
        DslParser $parser,
        DslValidator $validator,
        DslCompiler $compiler,
        SourceMetadataProvider $metadata,
        MailshotRepository $mailshots
    ) {
        $this->queries = $queries;
        $this->router = $router;
        $this->parser = $parser;
        $this->validator = $validator;
        $this->compiler = $compiler;
        $this->metadata = $metadata;
        $this->mailshots = $mailshots;
    }

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

    /** @return array<string, mixed>|null */
    public function get(int $id): ?array
    {
        return $this->queries->find($id);
    }

    /** @return array<string, mixed> */
    public function validateDsl(string $dsl): array
    {
        $ast = $this->parser->parse($dsl);
        $validation = $this->validator->validate($ast);
        $compiled = null;
        if ($validation['errors'] === []) {
            $compiled = $this->compiler->compile($ast);
        }

        return [
            'ast' => $ast,
            'errors' => $validation['errors'],
            'warnings' => $validation['warnings'],
            'expected_fields' => $validation['expected_fields'],
            'compiled_sql' => $compiled['sql'] ?? null,
        ];
    }

    /** @return array<string, mixed> */
    public function save(?int $id, string $name, string $dsl): array
    {
        $name = trim($name);
        $dsl = trim($dsl);

        if ($name === '') {
            return ['ok' => false, 'errors' => ['Name is required.']];
        }
        if ($dsl === '') {
            return ['ok' => false, 'errors' => ['DSL sentence is required.']];
        }

        $validation = $this->validateDsl($dsl);
        if ($validation['errors'] !== []) {
            return ['ok' => false, 'errors' => $validation['errors'], 'warnings' => $validation['warnings']];
        }

        if ($id === null) {
            $id = $this->queries->create(['name' => $name, 'dsl_text' => $dsl]);
        } else {
            $this->queries->update($id, ['name' => $name, 'dsl_text' => $dsl]);
        }

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

    public function delete(int $id): void
    {
        $row = $this->queries->find($id);
        if ($row === null) {
            return;
        }
        $name = trim((string) ($row['name'] ?? ''));
        if ($name !== '' && $this->mailshots->countByDataSource($name) > 0) {
            throw new \RuntimeException('Cannot delete data source: one or more mailshots reference this data source.');
        }
        $this->queries->delete($id);
    }

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

        $newId = $this->queries->create([
            'name' => $this->duplicateName((string) ($source['name'] ?? ''), $id),
            'dsl_text' => (string) ($source['dsl_text'] ?? ''),
        ]);

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

    /** @return array<string, mixed> */
    public function preview(string $dsl, int $limit = 50): array
    {
        $limit = max(1, min(200, $limit));
        return $this->queryRows($dsl, $limit);
    }

    /** @return array<string, mixed> */
    public function review(string $dsl): array
    {
        return $this->queryRows($dsl, null);
    }

    /** @return array<string, mixed> */
    private function queryRows(string $dsl, ?int $limit): array
    {
        $validation = $this->validateDsl($dsl);
        if ($validation['errors'] !== []) {
            return ['errors' => $validation['errors'], 'warnings' => $validation['warnings'], 'rows' => [], 'count' => 0, 'columns' => []];
        }

        $ast = $validation['ast'];
        $compiled = $this->compiler->compile($ast);

        $countSql = 'SELECT COUNT(*) FROM (' . $compiled['sql'] . ') AS q';
        $stmtCount = $this->router->membersPdo()->prepare($countSql);
        $stmtCount->execute($compiled['params']);
        $count = (int) $stmtCount->fetchColumn();

        $previewSql = $compiled['sql'];
        if ($limit !== null) {
            $previewSql .= ' LIMIT ' . $limit;
        }
        $stmtRows = $this->router->membersPdo()->prepare($previewSql);
        $stmtRows->execute($compiled['params']);
        $rows = $stmtRows->fetchAll(PDO::FETCH_ASSOC);

        $columnSet = [];
        foreach ($rows as $row) {
            foreach (array_keys($row) as $key) {
                $columnSet[$key] = true;
            }
        }

        return [
            'errors' => [],
            'warnings' => $validation['warnings'],
            'count' => $count,
            'rows' => $rows,
            'columns' => array_keys($columnSet),
            'expected_fields' => $validation['expected_fields'],
        ];
    }

    /** @return array<string, list<string>> */
    public function sourceFields(): array
    {
        $result = [];
        foreach ($this->metadata->allKnownSources() as $source) {
            $result[$source] = $this->metadata->sourceFields($source);
        }
        return $result;
    }

    /** @return list<string> */
    public function listSchemas(): array
    {
        $schemas = [];
        $membersSchema = $this->router->membersDbName();
        if ($membersSchema !== '') {
            $schemas[] = $membersSchema;
        }
        $fenSchema = $this->router->fenDbName();
        if ($fenSchema !== '') {
            $schemas[] = $fenSchema;
        }

        $sql = 'SELECT schema_name FROM information_schema.schemata ORDER BY schema_name';
        $rows = $this->router->membersPdo()->query($sql)->fetchAll(PDO::FETCH_ASSOC);
        foreach ($rows as $row) {
            $name = (string) ($row['schema_name'] ?? '');
            if ($name === '' || in_array($name, ['information_schema', 'performance_schema', 'mysql', 'sys'], true)) {
                continue;
            }
            $schemas[] = $name;
        }

        $schemas = array_values(array_unique($schemas));
        sort($schemas, SORT_NATURAL | SORT_FLAG_CASE);
        return $schemas;
    }

    /** @return list<string> */
    public function listTables(string $schema): array
    {
        $schema = trim($schema);
        if ($schema === '') {
            return [];
        }
        // Shared-hosting users may not have information_schema access.
        // Use explicit SHOW TABLES for the selected schema.
        $sql = 'SHOW TABLES FROM `' . str_replace('`', '``', $schema) . '`';
        $rows = $this->router->membersPdo()->query($sql)->fetchAll(PDO::FETCH_NUM);
        $tables = [];
        foreach ($rows as $row) {
            if (isset($row[0]) && trim((string) $row[0]) !== '') {
                $tables[] = (string) $row[0];
            }
        }
        sort($tables, SORT_NATURAL | SORT_FLAG_CASE);
        return $tables;
    }

    /** @return list<string> */
    public function sourceFieldsForSource(string $source): array
    {
        $source = trim($source);
        if ($source === '') {
            return [];
        }
        return $this->metadata->sourceFields($source);
    }

    private function duplicateName(string $name, int $sourceId): string
    {
        $base = trim($name);
        if ($base === '') {
            $base = 'Data source #' . $sourceId;
        }

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

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

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

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

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