<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Service;

use OCA\Mitgliederverwaltung\Db\AddressMapper;
use OCA\Mitgliederverwaltung\Db\EmailMapper;
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
use OCA\Mitgliederverwaltung\Db\FeeRecordMapper;
use OCA\Mitgliederverwaltung\Db\FeeRuleMapper;
use OCA\Mitgliederverwaltung\Db\InjuryMapper;
use OCA\Mitgliederverwaltung\Db\LagerMapper;
use OCA\Mitgliederverwaltung\Db\LagerTeilnehmerMapper;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Db\SavedQueryMapper;
use OCA\Mitgliederverwaltung\Db\StufeHistoryMapper;
use OCA\Mitgliederverwaltung\Db\StufeMapper;
use Psr\Log\LoggerInterface;

/**
 * Generic per-entity CSV export service.
 *
 * Exports any entity type as a semicolon-delimited CSV with German headers,
 * UTF-8 BOM, and foreign-key resolution. Encrypted fields show [verschluesselt]
 * by default unless includeSensitive is true (used by ZIP bundle export).
 *
 * Column definitions are reusable by the import service for header auto-mapping.
 *
 * Part of Issue #148.
 */
class EntityExportService {

    private MemberMapper $memberMapper;
    private FamilyMapper $familyMapper;
    private AddressMapper $addressMapper;
    private PhoneMapper $phoneMapper;
    private EmailMapper $emailMapper;
    private StufeMapper $stufeMapper;
    private StufeHistoryMapper $stufeHistoryMapper;
    private FeeRuleMapper $feeRuleMapper;
    private FeeRecordMapper $feeRecordMapper;
    private LagerMapper $lagerMapper;
    private LagerTeilnehmerMapper $lagerTeilnehmerMapper;
    private InjuryMapper $injuryMapper;
    private SavedQueryMapper $savedQueryMapper;
    private EncryptionService $encryptionService;
    private LoggerInterface $logger;

    /** UTF-8 BOM for Excel compatibility */
    private const UTF8_BOM = "\xEF\xBB\xBF";

    /** CSV delimiter — semicolon for German locale */
    private const DELIMITER = ';';

    /** Placeholder for encrypted fields */
    private const ENCRYPTED_PLACEHOLDER = '[verschluesselt]';

    /** Lookup caches (lazy-initialized) */
    private ?array $stufeNames = null;
    private ?array $familyNames = null;
    private ?array $memberNames = null;
    private ?array $lagerNames = null;

    /**
     * Entity type definitions: key => [label, filename].
     * Order matters for ZIP bundle naming.
     */
    public const ENTITY_TYPES = [
        'mitglieder'           => ['label' => 'Mitglieder',            'filename' => 'Mitglieder.csv'],
        'familien'             => ['label' => 'Familien',              'filename' => 'Familien.csv'],
        'adressen'             => ['label' => 'Adressen',             'filename' => 'Adressen.csv'],
        'telefonnummern'       => ['label' => 'Telefonnummern',       'filename' => 'Telefonnummern.csv'],
        'emails'               => ['label' => 'E-Mails',             'filename' => 'E-Mails.csv'],
        'stufen'               => ['label' => 'Stufen',              'filename' => 'Stufen.csv'],
        'stufenverlauf'        => ['label' => 'Stufenverlauf',       'filename' => 'Stufenverlauf.csv'],
        'beitragsregeln'       => ['label' => 'Beitragsregeln',      'filename' => 'Beitragsregeln.csv'],
        'beitraege'            => ['label' => 'Beitraege',           'filename' => 'Beitraege.csv'],
        'lager'                => ['label' => 'Lager',               'filename' => 'Lager.csv'],
        'lagerteilnehmer'      => ['label' => 'Lagerteilnehmer',     'filename' => 'Lagerteilnehmer.csv'],
        'verletzungen'         => ['label' => 'Verletzungen',        'filename' => 'Verletzungen.csv'],
        'gespeicherte_abfragen' => ['label' => 'Gespeicherte Abfragen', 'filename' => 'Gespeicherte_Abfragen.csv'],
    ];

    public function __construct(
        MemberMapper $memberMapper,
        FamilyMapper $familyMapper,
        AddressMapper $addressMapper,
        PhoneMapper $phoneMapper,
        EmailMapper $emailMapper,
        StufeMapper $stufeMapper,
        StufeHistoryMapper $stufeHistoryMapper,
        FeeRuleMapper $feeRuleMapper,
        FeeRecordMapper $feeRecordMapper,
        LagerMapper $lagerMapper,
        LagerTeilnehmerMapper $lagerTeilnehmerMapper,
        InjuryMapper $injuryMapper,
        SavedQueryMapper $savedQueryMapper,
        EncryptionService $encryptionService,
        LoggerInterface $logger
    ) {
        $this->memberMapper = $memberMapper;
        $this->familyMapper = $familyMapper;
        $this->addressMapper = $addressMapper;
        $this->phoneMapper = $phoneMapper;
        $this->emailMapper = $emailMapper;
        $this->stufeMapper = $stufeMapper;
        $this->stufeHistoryMapper = $stufeHistoryMapper;
        $this->feeRuleMapper = $feeRuleMapper;
        $this->feeRecordMapper = $feeRecordMapper;
        $this->lagerMapper = $lagerMapper;
        $this->lagerTeilnehmerMapper = $lagerTeilnehmerMapper;
        $this->injuryMapper = $injuryMapper;
        $this->savedQueryMapper = $savedQueryMapper;
        $this->encryptionService = $encryptionService;
        $this->logger = $logger;
    }

    // ── Public API ──────────────────────────────────────────────────

    /**
     * Get all available entity types for export.
     *
     * @return array List of {key, label} objects
     */
    public function getEntityTypes(): array {
        $types = [];
        foreach (self::ENTITY_TYPES as $key => $meta) {
            $types[] = ['key' => $key, 'label' => $meta['label']];
        }
        return $types;
    }

    /**
     * Get column header definitions for an entity type.
     * Reusable by import for German header → field auto-mapping.
     *
     * @return string[] German column headers
     */
    public function getColumnHeaders(string $type): array {
        return match ($type) {
            'mitglieder' => ['ID', 'Vorname', 'Nachname', 'Geburtsdatum', 'Geschlecht', 'Rolle', 'Stufe-ID', 'Stufenname', 'Eintritt', 'Austritt', 'Status', 'Allergien', 'Notizen', 'Zusaetzliche Notizen', 'KV-Typ', 'KV-Name', 'Familien-ID', 'Familienname', 'Eingefrorener Beitragssatz', 'Einwilligungsdatum', 'Juleica-Nummer', 'Juleica-Ablaufdatum'],
            'familien' => ['ID', 'Name', 'Kontoinhaber', 'IBAN', 'BIC', 'Kreditinstitut'],
            'adressen' => ['ID', 'Mitglied-ID', 'Mitgliedername', 'Label', 'Strasse', 'PLZ', 'Ort', 'Land', 'Primaer'],
            'telefonnummern' => ['ID', 'Mitglied-ID', 'Mitgliedername', 'Label', 'Nummer'],
            'emails' => ['ID', 'Mitglied-ID', 'Mitgliedername', 'Label', 'E-Mail-Adresse'],
            'stufen' => ['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe'],
            'stufenverlauf' => ['ID', 'Mitglied-ID', 'Mitgliedername', 'Stufe-ID', 'Stufenname', 'Datum', 'Geaendert von'],
            'beitragsregeln' => ['ID', 'Gueltig ab Jahr', 'Grundbeitrag', 'Familienregeln', 'Inaktiven-Regel', 'Erstellt am', 'Erstellt von'],
            'beitraege' => ['ID', 'Mitglied-ID', 'Mitgliedername', 'Jahr', 'Betrag', 'Bezahlt', 'Zahlungsdatum', 'Manuell angepasst', 'Notizen'],
            'lager' => ['ID', 'Name', 'Startdatum', 'Enddatum', 'Ort', 'Beschreibung'],
            'lagerteilnehmer' => ['ID', 'Lager-ID', 'Lagername', 'Mitglied-ID', 'Mitgliedername', 'Rolle'],
            'verletzungen' => ['ID', 'Datum', 'Mitglied-ID', 'Mitgliedername', 'Lager-ID', 'Lagername', 'Aktivitaet', 'Beschreibung', 'Erstellt von'],
            'gespeicherte_abfragen' => ['ID', 'Name', 'Abfrage', 'Sortierung', 'Erstellt von', 'Erstellt am'],
            default => throw new \InvalidArgumentException("Unbekannter Entity-Typ: $type"),
        };
    }

    /**
     * Export a single entity type as CSV.
     *
     * @param string $type Entity type key
     * @param bool $includeSensitive If true, decrypt sensitive fields instead of masking
     * @return array{content: string, filename: string}
     */
    public function exportEntity(string $type, bool $includeSensitive = false): array {
        if (!isset(self::ENTITY_TYPES[$type])) {
            throw new \InvalidArgumentException("Unbekannter Entity-Typ: $type");
        }

        $this->resetCaches();

        $headers = $this->getColumnHeaders($type);
        $rows = match ($type) {
            'mitglieder' => $this->buildMitgliederRows($includeSensitive),
            'familien' => $this->buildFamilienRows($includeSensitive),
            'adressen' => $this->buildAdressenRows(),
            'telefonnummern' => $this->buildTelefonnummernRows(),
            'emails' => $this->buildEmailsRows(),
            'stufen' => $this->buildStufenRows(),
            'stufenverlauf' => $this->buildStufenverlaufRows(),
            'beitragsregeln' => $this->buildBeitragsregelnRows(),
            'beitraege' => $this->buildBeitraegeRows(),
            'lager' => $this->buildLagerRows(),
            'lagerteilnehmer' => $this->buildLagerteilnehmerRows(),
            'verletzungen' => $this->buildVerletzungenRows(),
            'gespeicherte_abfragen' => $this->buildGespeicherteAbfragenRows(),
        };

        return [
            'content' => $this->buildCsv($headers, $rows),
            'filename' => self::ENTITY_TYPES[$type]['filename'],
        ];
    }

    // ── Row builders ────────────────────────────────────────────────

    private function buildMitgliederRows(bool $includeSensitive): array {
        $members = $this->memberMapper->findAll();
        $rows = [];
        foreach ($members as $m) {
            $allergien = self::ENCRYPTED_PLACEHOLDER;
            if ($includeSensitive && $m->getAllergienEncrypted()) {
                $allergien = $this->encryptionService->decrypt($m->getAllergienEncrypted());
            } elseif (!$m->getAllergienEncrypted()) {
                $allergien = '';
            }

            $rows[] = [
                (string)$m->getId(),
                $m->getVorname(),
                $m->getNachname(),
                $m->getGeburtsdatum() ?? '',
                $m->getGeschlecht() ?? '',
                $m->getRolle() ?? '',
                (string)($m->getStufeId() ?? ''),
                $this->resolveStufe($m->getStufeId()),
                $m->getEintritt() ?? '',
                $m->getAustritt() ?? '',
                $m->getStatus() ?? '',
                $allergien,
                $m->getNotizen() ?? '',
                $m->getZusatzNotizen() ?? '',
                $m->getKvTyp() ?? '',
                $m->getKvName() ?? '',
                (string)($m->getFamilyId() ?? ''),
                $this->resolveFamily($m->getFamilyId()),
                (string)($m->getFrozenFeeRate() ?? ''),
                $m->getEinwilligungDatum() ?? '',
                $m->getJuleicaNummer() ?? '',
                $m->getJuleicaAblaufdatum() ?? '',
            ];
        }
        return $rows;
    }

    private function buildFamilienRows(bool $includeSensitive): array {
        $families = $this->familyMapper->findAll();
        $rows = [];
        foreach ($families as $f) {
            $kontoinhaber = self::ENCRYPTED_PLACEHOLDER;
            $iban = self::ENCRYPTED_PLACEHOLDER;
            if ($includeSensitive) {
                $kontoinhaber = $f->getKontoinhaberEncrypted()
                    ? $this->encryptionService->decrypt($f->getKontoinhaberEncrypted())
                    : '';
                $iban = $f->getIbanEncrypted()
                    ? $this->encryptionService->decrypt($f->getIbanEncrypted())
                    : '';
            } else {
                if (!$f->getKontoinhaberEncrypted()) {
                    $kontoinhaber = '';
                }
                if (!$f->getIbanEncrypted()) {
                    $iban = '';
                }
            }

            $rows[] = [
                (string)$f->getId(),
                $f->getName(),
                $kontoinhaber,
                $iban,
                $f->getBic() ?? '',
                $f->getKreditinstitut() ?? '',
            ];
        }
        return $rows;
    }

    private function buildAdressenRows(): array {
        $addresses = $this->addressMapper->findAll();
        $rows = [];
        foreach ($addresses as $a) {
            $rows[] = [
                (string)$a->getId(),
                (string)$a->getMemberId(),
                $this->resolveMember($a->getMemberId()),
                $a->getLabel() ?? '',
                $a->getStrasse() ?? '',
                $a->getPlz() ?? '',
                $a->getOrt() ?? '',
                $a->getLand() ?? '',
                $a->getIsPrimary() ? 'Ja' : 'Nein',
            ];
        }
        return $rows;
    }

    private function buildTelefonnummernRows(): array {
        $phones = $this->phoneMapper->findAll();
        $rows = [];
        foreach ($phones as $p) {
            $rows[] = [
                (string)$p->getId(),
                (string)$p->getMemberId(),
                $this->resolveMember($p->getMemberId()),
                $p->getLabel() ?? '',
                $p->getNumberE164() ?? '',
            ];
        }
        return $rows;
    }

    private function buildEmailsRows(): array {
        $emails = $this->emailMapper->findAll();
        $rows = [];
        foreach ($emails as $e) {
            $rows[] = [
                (string)$e->getId(),
                (string)$e->getMemberId(),
                $this->resolveMember($e->getMemberId()),
                $e->getLabel() ?? '',
                $e->getEmail() ?? '',
            ];
        }
        return $rows;
    }

    private function buildStufenRows(): array {
        $stufen = $this->stufeMapper->findAll();
        $rows = [];
        foreach ($stufen as $s) {
            $rows[] = [
                (string)$s->getId(),
                $s->getName(),
                (string)($s->getSortOrder() ?? ''),
                (string)($s->getAgeRangeMin() ?? ''),
                (string)($s->getAgeRangeMax() ?? ''),
                $s->getColor() ?? '',
            ];
        }
        return $rows;
    }

    private function buildStufenverlaufRows(): array {
        $history = $this->stufeHistoryMapper->findAll();
        $rows = [];
        foreach ($history as $h) {
            $rows[] = [
                (string)$h->getId(),
                (string)$h->getMemberId(),
                $this->resolveMember($h->getMemberId()),
                (string)$h->getStufeId(),
                $this->resolveStufe($h->getStufeId()),
                $h->getChangedAt() ?? '',
                $h->getChangedBy() ?? '',
            ];
        }
        return $rows;
    }

    private function buildBeitragsregelnRows(): array {
        $rules = $this->feeRuleMapper->findAll();
        $rows = [];
        foreach ($rules as $r) {
            $rows[] = [
                (string)$r->getId(),
                (string)$r->getYearFrom(),
                (string)($r->getBaseRate() ?? ''),
                $r->getFamilyRulesJson() ?? '',
                (string)($r->getInactiveRule() ?? ''),
                $r->getCreatedAt() ?? '',
                $r->getCreatedBy() ?? '',
            ];
        }
        return $rows;
    }

    private function buildBeitraegeRows(): array {
        $records = $this->feeRecordMapper->findAll();
        $rows = [];
        foreach ($records as $r) {
            $rows[] = [
                (string)$r->getId(),
                (string)$r->getMemberId(),
                $this->resolveMember($r->getMemberId()),
                (string)$r->getYear(),
                (string)($r->getAmount() ?? '0.00'),
                $r->getPaid() ? 'Ja' : 'Nein',
                $r->getPaymentDate() ?? '',
                $r->getManuellAngepasst() ? 'Ja' : 'Nein',
                $r->getNotes() ?? '',
            ];
        }
        return $rows;
    }

    private function buildLagerRows(): array {
        $lager = $this->lagerMapper->findAll();
        $rows = [];
        foreach ($lager as $l) {
            $rows[] = [
                (string)$l->getId(),
                $l->getName(),
                $l->getStartdatum() ?? '',
                $l->getEnddatum() ?? '',
                $l->getOrt() ?? '',
                $l->getBeschreibung() ?? '',
            ];
        }
        return $rows;
    }

    private function buildLagerteilnehmerRows(): array {
        $teilnehmer = $this->lagerTeilnehmerMapper->findAll();
        $rows = [];
        foreach ($teilnehmer as $t) {
            $rows[] = [
                (string)$t->getId(),
                (string)$t->getLagerId(),
                $this->resolveLager($t->getLagerId()),
                (string)$t->getMemberId(),
                $this->resolveMember($t->getMemberId()),
                $t->getRolle() ?? '',
            ];
        }
        return $rows;
    }

    private function buildVerletzungenRows(): array {
        $injuries = $this->injuryMapper->findAll();
        $rows = [];
        foreach ($injuries as $i) {
            $rows[] = [
                (string)$i->getId(),
                $i->getDatum() ?? '',
                (string)($i->getMemberId() ?? ''),
                $this->resolveMember($i->getMemberId()),
                (string)($i->getLagerId() ?? ''),
                $this->resolveLager($i->getLagerId()),
                $i->getActivityName() ?? '',
                $i->getBeschreibung() ?? '',
                $i->getCreatedBy() ?? '',
            ];
        }
        return $rows;
    }

    private function buildGespeicherteAbfragenRows(): array {
        $queries = $this->savedQueryMapper->findAll();
        $rows = [];
        foreach ($queries as $q) {
            $rows[] = [
                (string)$q->getId(),
                $q->getName(),
                $q->getQueryJson() ?? '',
                $q->getSortJson() ?? '',
                $q->getCreatedBy() ?? '',
                $q->getCreatedAt() ?? '',
            ];
        }
        return $rows;
    }

    // ── Foreign key resolution ──────────────────────────────────────

    private function resolveStufe(?int $id): string {
        if ($id === null) {
            return '';
        }
        if ($this->stufeNames === null) {
            $this->stufeNames = [];
            foreach ($this->stufeMapper->findAll() as $s) {
                $this->stufeNames[$s->getId()] = $s->getName();
            }
        }
        return $this->stufeNames[$id] ?? '';
    }

    private function resolveFamily(?int $id): string {
        if ($id === null) {
            return '';
        }
        if ($this->familyNames === null) {
            $this->familyNames = [];
            foreach ($this->familyMapper->findAll() as $f) {
                $this->familyNames[$f->getId()] = $f->getName();
            }
        }
        return $this->familyNames[$id] ?? '';
    }

    private function resolveMember(?int $id): string {
        if ($id === null) {
            return '';
        }
        if ($this->memberNames === null) {
            $this->memberNames = [];
            foreach ($this->memberMapper->findAll() as $m) {
                $this->memberNames[$m->getId()] = $m->getVorname() . ' ' . $m->getNachname();
            }
        }
        return $this->memberNames[$id] ?? '';
    }

    private function resolveLager(?int $id): string {
        if ($id === null) {
            return '';
        }
        if ($this->lagerNames === null) {
            $this->lagerNames = [];
            foreach ($this->lagerMapper->findAll() as $l) {
                $this->lagerNames[$l->getId()] = $l->getName();
            }
        }
        return $this->lagerNames[$id] ?? '';
    }

    private function resetCaches(): void {
        $this->stufeNames = null;
        $this->familyNames = null;
        $this->memberNames = null;
        $this->lagerNames = null;
    }

    // ── ZIP bundle export (Issue #149) ─────────────────────────────

    /**
     * Export all entity types as a single ZIP bundle.
     *
     * Each entity is exported as a CSV inside the ZIP using the German
     * filename convention defined in ENTITY_TYPES. The ZIP is written
     * to a temporary file and streamed, not buffered entirely in memory.
     *
     * @param bool $includeSensitive If true, decrypt sensitive fields
     * @return array{path: string, filename: string} Temp file path and download name
     */
    public function exportFullBundle(bool $includeSensitive = false): array {
        $this->resetCaches();

        if (!class_exists(\ZipArchive::class)) {
            throw new \RuntimeException(
                'ZipArchive ist nicht verfuegbar. Bitte php-zip installieren.'
            );
        }

        $tmpFile = tempnam(sys_get_temp_dir(), 'mv_bundle_');
        if ($tmpFile === false) {
            throw new \RuntimeException('Temporaere Datei konnte nicht erstellt werden.');
        }

        // Restrict permissions to owner-only to prevent other processes
        // from reading export data during the temp file's lifetime
        chmod($tmpFile, 0600);

        $zip = new \ZipArchive();
        $result = $zip->open($tmpFile, \ZipArchive::OVERWRITE);
        if ($result !== true) {
            unlink($tmpFile);
            throw new \RuntimeException(
                'ZIP-Archiv konnte nicht erstellt werden. Fehlercode: ' . $result
            );
        }

        foreach (self::ENTITY_TYPES as $type => $meta) {
            $this->resetCaches();
            $export = $this->exportEntity($type, $includeSensitive);
            $zip->addFromString($meta['filename'], $export['content']);
        }

        $zip->close();

        $date = (new \DateTime())->format('Y-m-d_His');
        $suffix = $includeSensitive ? '_mit_sensiblen_Daten' : '';
        $filename = "Vollstaendiger_Export{$suffix}_{$date}.zip";

        return [
            'path' => $tmpFile,
            'filename' => $filename,
        ];
    }

    // ── CSV builder ─────────────────────────────────────────────────

    /**
     * Build a CSV string from headers and row data.
     */
    public function buildCsv(array $headers, array $rows): string {
        $output = self::UTF8_BOM;
        $output .= $this->encodeCsvLine($headers);
        foreach ($rows as $row) {
            $output .= $this->encodeCsvLine($row);
        }
        return $output;
    }

    /**
     * Encode a single CSV line with proper escaping and formula injection prevention.
     */
    private function encodeCsvLine(array $fields): string {
        $escaped = array_map(function (string $field): string {
            $field = CsvExportService::sanitizeCsvField($field);

            if (
                str_contains($field, self::DELIMITER) ||
                str_contains($field, '"') ||
                str_contains($field, "\n") ||
                str_contains($field, "\r")
            ) {
                return '"' . str_replace('"', '""', $field) . '"';
            }
            return $field;
        }, $fields);

        return implode(self::DELIMITER, $escaped) . "\r\n";
    }
}
