<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Service;

use Psr\Log\LoggerInterface;

/**
 * Service for importing ZIP bundles containing multiple entity CSVs.
 *
 * Handles:
 * - ZIP extraction and entity type detection by filename or header analysis
 * - Topological sort of entity dependencies for correct import order
 * - ID remapping across entities (old ID -> new ID)
 * - Per-entity import via EntityImportService
 *
 * Part of Issue #151.
 */
class BundleImportService {

    private EntityImportService $entityImportService;
    private EntityExportService $exportService;
    private LoggerInterface $logger;

    /**
     * Entity dependency graph for topological sort.
     * Key = entity type, Value = array of entity types it depends on.
     */
    private const DEPENDENCY_GRAPH = [
        'stufen' => [],
        'familien' => [],
        'mitglieder' => ['stufen', 'familien'],
        'adressen' => ['mitglieder'],
        'telefonnummern' => ['mitglieder'],
        'emails' => ['mitglieder'],
        'stufenverlauf' => ['mitglieder', 'stufen'],
        'beitragsregeln' => [],
        'beitraege' => ['mitglieder'],
        'lager' => [],
        'lagerteilnehmer' => ['lager', 'mitglieder'],
        'verletzungen' => ['mitglieder', 'lager'],
        'gespeicherte_abfragen' => [],
    ];

    /**
     * Filename -> entity type mapping (from EntityExportService::ENTITY_TYPES).
     */
    private array $filenameMap = [];

    public function __construct(
        EntityImportService $entityImportService,
        EntityExportService $exportService,
        LoggerInterface $logger
    ) {
        $this->entityImportService = $entityImportService;
        $this->exportService = $exportService;
        $this->logger = $logger;

        // Build filename lookup
        foreach (EntityExportService::ENTITY_TYPES as $type => $meta) {
            $this->filenameMap[strtolower($meta['filename'])] = $type;
        }
    }

    /**
     * Analyze a ZIP file: extract CSVs, detect entity types, determine import order.
     *
     * @param string $zipContent Raw ZIP binary content
     * @return array{entities: array[], importOrder: string[], unknown: string[]}
     */
    public function analyzeBundle(string $zipContent): array {
        $csvFiles = $this->extractCsvFiles($zipContent);

        $entities = [];
        $unknown = [];

        foreach ($csvFiles as $filename => $content) {
            $type = $this->detectTypeByFilename($filename);

            if ($type === null) {
                // Fall back to header-based detection
                try {
                    $parsed = $this->entityImportService->parseFile($content, ';', 'UTF-8');
                    $detection = $this->entityImportService->detectEntityType($parsed['columns']);
                    $type = $detection['type'];
                } catch (\Exception $e) {
                    // Cannot parse this file
                    $type = null;
                }
            }

            if ($type !== null) {
                // Count rows
                try {
                    $parsed = $this->entityImportService->parseFile($content, ';', 'UTF-8');
                    $rowCount = $parsed['totalRows'];
                } catch (\Exception $e) {
                    $rowCount = 0;
                }

                $entities[] = [
                    'filename' => $filename,
                    'type' => $type,
                    'label' => EntityExportService::ENTITY_TYPES[$type]['label'] ?? $type,
                    'rowCount' => $rowCount,
                ];
            } else {
                $unknown[] = $filename;
            }
        }

        // Determine import order based on dependency graph
        $detectedTypes = array_map(fn($e) => $e['type'], $entities);
        $importOrder = $this->topologicalSort($detectedTypes);

        // Sort entities array by import order
        $orderIndex = array_flip($importOrder);
        usort($entities, function ($a, $b) use ($orderIndex) {
            $ia = $orderIndex[$a['type']] ?? PHP_INT_MAX;
            $ib = $orderIndex[$b['type']] ?? PHP_INT_MAX;
            return $ia <=> $ib;
        });

        return [
            'entities' => $entities,
            'importOrder' => $importOrder,
            'unknown' => $unknown,
        ];
    }

    /**
     * Execute bundle import: import all entities in dependency order with ID remapping.
     *
     * @param string $zipContent Raw ZIP binary content
     * @param array $entityOverrides Optional type overrides: {filename: type}
     * @return array{results: array[], idMap: array}
     */
    public function executeBundle(string $zipContent, array $entityOverrides = [], array $corrections = []): array {
        $csvFiles = $this->extractCsvFiles($zipContent);
        $analysis = $this->analyzeBundle($zipContent);

        // Apply overrides
        foreach ($analysis['entities'] as &$entity) {
            if (isset($entityOverrides[$entity['filename']])) {
                $entity['type'] = $entityOverrides[$entity['filename']];
            }
        }

        // ID remapping table: type => {oldId => newId}
        $idMap = [];
        $results = [];

        foreach ($analysis['importOrder'] as $type) {
            // Find the entity entry for this type
            $entityEntry = null;
            foreach ($analysis['entities'] as $e) {
                if ($e['type'] === $type) {
                    $entityEntry = $e;
                    break;
                }
            }

            if ($entityEntry === null) {
                continue; // This type is not in the ZIP
            }

            $filename = $entityEntry['filename'];
            $content = $csvFiles[$filename] ?? null;
            if ($content === null) {
                $results[] = [
                    'type' => $type,
                    'label' => $entityEntry['label'],
                    'status' => 'error',
                    'error' => 'CSV-Datei nicht gefunden',
                    'created' => 0,
                    'skipped' => 0,
                    'errors' => 1,
                ];
                continue;
            }

            try {
                // Apply ID remapping to the CSV content before import
                $remappedContent = $this->applyIdRemapping($type, $content, $idMap);

                // Parse, auto-map, and execute
                $parsed = $this->entityImportService->parseFile($remappedContent, ';', 'UTF-8');
                $mapping = $this->entityImportService->autoMapHeaders($type, $parsed['columns']);

                $entityCorrections = $corrections[$type] ?? [];

                $result = $this->entityImportService->execute(
                    $type,
                    $remappedContent,
                    $mapping,
                    ';',
                    'UTF-8',
                    false,
                    $entityCorrections
                );

                // Collect ID mappings from created entities
                $this->collectIdMappings($type, $content, $result, $idMap);

                $results[] = [
                    'type' => $type,
                    'label' => $entityEntry['label'],
                    'status' => 'success',
                    'created' => $result['created'],
                    'skipped' => $result['skipped'],
                    'errors' => $result['errors'],
                    'details' => $result['details'],
                ];
            } catch (\Exception $e) {
                $this->logger->error('Bundle import failed for entity type', [
                    'type' => $type,
                    'exception' => $e,
                    'app' => 'mitgliederverwaltung',
                ]);

                $results[] = [
                    'type' => $type,
                    'label' => $entityEntry['label'],
                    'status' => 'error',
                    'error' => $e->getMessage(),
                    'created' => 0,
                    'skipped' => 0,
                    'errors' => 1,
                ];
            }
        }

        return [
            'results' => $results,
            'idMap' => $idMap,
        ];
    }

    /**
     * Preview bundle import (dry-run for all entities).
     *
     * @param string $zipContent Raw ZIP binary content
     * @param array $corrections Per-entity corrections: { entityType: { rowNum: { fieldKey: value } } }
     * @return array{results: array[]}
     */
    public function previewBundle(string $zipContent, array $corrections = []): array {
        $csvFiles = $this->extractCsvFiles($zipContent);
        $analysis = $this->analyzeBundle($zipContent);
        $results = [];

        foreach ($analysis['importOrder'] as $type) {
            $entityEntry = null;
            foreach ($analysis['entities'] as $e) {
                if ($e['type'] === $type) {
                    $entityEntry = $e;
                    break;
                }
            }

            if ($entityEntry === null) continue;

            $filename = $entityEntry['filename'];
            $content = $csvFiles[$filename] ?? null;
            if ($content === null) continue;

            try {
                $parsed = $this->entityImportService->parseFile($content, ';', 'UTF-8');
                $mapping = $this->entityImportService->autoMapHeaders($type, $parsed['columns']);

                $entityCorrections = $corrections[$type] ?? [];

                $preview = $this->entityImportService->preview(
                    $type,
                    $content,
                    $mapping,
                    ';',
                    'UTF-8',
                    false,
                    $entityCorrections
                );

                // Include target fields so the frontend can render editable inputs
                $schema = $this->entityImportService->getImportSchema($type);

                $results[] = [
                    'type' => $type,
                    'label' => $entityEntry['label'],
                    'summary' => $preview['summary'],
                    'fkWarnings' => $preview['fkWarnings'],
                    'errorCount' => count($preview['errors']),
                    'errors' => array_slice($preview['errors'], 0, 10),
                    'targetFields' => $schema['targetFields'] ?? [],
                ];
            } catch (\Exception $e) {
                $results[] = [
                    'type' => $type,
                    'label' => $entityEntry['label'],
                    'summary' => ['total' => 0, 'toCreate' => 0, 'duplicates' => 0, 'errors' => 1],
                    'fkWarnings' => [],
                    'errorCount' => 1,
                    'errors' => [['_rowIndex' => 0, '_errors' => [$e->getMessage()]]],
                    'targetFields' => [],
                ];
            }
        }

        return ['results' => $results];
    }

    // ── ZIP extraction ─────────────────────────────────────────────

    /**
     * Extract all CSV files from a ZIP archive.
     *
     * @param string $zipContent Raw ZIP binary
     * @return array<string, string> Filename => CSV content
     */
    private function extractCsvFiles(string $zipContent): array {
        if (!class_exists(\ZipArchive::class)) {
            throw new \RuntimeException('ZipArchive ist nicht verfuegbar.');
        }

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

        // Restrict permissions to owner-only before writing content
        chmod($tmpFile, 0600);

        try {
            file_put_contents($tmpFile, $zipContent);

            $size = @filesize($tmpFile);
            if ($size === 0 || $size === false) {
                throw new ValidationException('Die hochgeladene Datei ist leer.');
            }

            $zip = new \ZipArchive();
            $result = $zip->open($tmpFile);
            if ($result !== true) {
                throw new ValidationException('ZIP-Datei konnte nicht geoeffnet werden. Fehlercode: ' . $result);
            }

            $csvFiles = [];
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $name = $zip->getNameIndex($i);
                if ($name === false) {
                    continue;
                }

                // Reject path traversal: entries containing '..' or absolute paths
                if (str_contains($name, '..') || str_starts_with($name, '/') || str_starts_with($name, '\\')) {
                    $this->logger->warning('Skipping ZIP entry with suspicious path', [
                        'entry' => $name,
                        'app' => 'mitgliederverwaltung',
                    ]);
                    continue;
                }

                $baseName = basename($name);

                // Only include CSV files (skip directories, hidden files)
                if (
                    !str_starts_with($baseName, '.') &&
                    !str_starts_with($baseName, '__') &&
                    (str_ends_with(strtolower($baseName), '.csv') || str_ends_with(strtolower($baseName), '.txt'))
                ) {
                    $content = $zip->getFromIndex($i);
                    if ($content !== false) {
                        $csvFiles[$baseName] = $content;
                    }
                }
            }

            $zip->close();

            if (empty($csvFiles)) {
                throw new ValidationException('ZIP-Archiv enthaelt keine CSV-Dateien.');
            }

            return $csvFiles;
        } finally {
            if (file_exists($tmpFile)) {
                unlink($tmpFile);
            }
        }
    }

    // ── Entity type detection ──────────────────────────────────────

    /**
     * Detect entity type by filename matching against known export filenames.
     */
    private function detectTypeByFilename(string $filename): ?string {
        return $this->filenameMap[strtolower($filename)] ?? null;
    }

    // ── Topological sort ───────────────────────────────────────────

    /**
     * Topological sort of entity types based on dependency graph.
     * Only includes types present in the $types array.
     *
     * @param string[] $types Entity types to sort
     * @return string[] Sorted import order
     */
    private function topologicalSort(array $types): array {
        $typeSet = array_flip($types);
        $sorted = [];
        $visited = [];

        foreach ($types as $type) {
            $this->visit($type, $typeSet, $visited, $sorted);
        }

        return $sorted;
    }

    private function visit(string $type, array $typeSet, array &$visited, array &$sorted): void {
        if (isset($visited[$type])) {
            return;
        }
        $visited[$type] = true;

        $deps = self::DEPENDENCY_GRAPH[$type] ?? [];
        foreach ($deps as $dep) {
            if (isset($typeSet[$dep])) {
                $this->visit($dep, $typeSet, $visited, $sorted);
            }
        }

        $sorted[] = $type;
    }

    // ── ID remapping ───────────────────────────────────────────────

    /**
     * Apply ID remapping to CSV content based on previously imported entities.
     *
     * Replaces old IDs in FK columns with new IDs from the idMap.
     */
    private function applyIdRemapping(string $type, string $content, array $idMap): string {
        // Determine which columns need remapping for this type
        $fkColumns = $this->getFkColumnsForRemapping($type);
        if (empty($fkColumns)) {
            return $content;
        }

        // Parse the CSV
        $lines = explode("\n", str_replace("\r\n", "\n", $content));
        if (count($lines) < 2) {
            return $content;
        }

        // Check for BOM
        $bom = '';
        if (substr($lines[0], 0, 3) === "\xEF\xBB\xBF") {
            $bom = "\xEF\xBB\xBF";
            $lines[0] = substr($lines[0], 3);
        }

        // Parse header to find column indices
        $headers = str_getcsv($lines[0], ';');
        $headers = array_map('strtolower', array_map('trim', $headers));

        $remapIndices = [];
        foreach ($fkColumns as $colInfo) {
            $headerIdx = array_search(strtolower($colInfo['header']), $headers);
            if ($headerIdx !== false) {
                $remapIndices[$headerIdx] = $colInfo['source_type'];
            }
        }

        if (empty($remapIndices)) {
            return $content;
        }

        // Rebuild the CSV with remapped IDs
        $output = $bom . $lines[0] . "\r\n";
        for ($i = 1; $i < count($lines); $i++) {
            $line = trim($lines[$i]);
            if ($line === '') continue;

            $row = str_getcsv($line, ';');
            foreach ($remapIndices as $colIdx => $sourceType) {
                if (isset($row[$colIdx]) && $row[$colIdx] !== '' && isset($idMap[$sourceType])) {
                    $oldId = $row[$colIdx];
                    if (isset($idMap[$sourceType][$oldId])) {
                        $row[$colIdx] = (string)$idMap[$sourceType][$oldId];
                    }
                }
            }

            // Re-encode as CSV line
            $escaped = array_map(function ($field) {
                if (str_contains($field, ';') || str_contains($field, '"') || str_contains($field, "\n")) {
                    return '"' . str_replace('"', '""', $field) . '"';
                }
                return $field;
            }, $row);
            $output .= implode(';', $escaped) . "\r\n";
        }

        return $output;
    }

    /**
     * Get FK columns that need ID remapping for a given entity type.
     *
     * @return array[] Each: {header, source_type}
     */
    private function getFkColumnsForRemapping(string $type): array {
        return match ($type) {
            'mitglieder' => [
                ['header' => 'stufe-id', 'source_type' => 'stufen'],
                ['header' => 'familien-id', 'source_type' => 'familien'],
            ],
            'adressen', 'telefonnummern', 'emails' => [
                ['header' => 'mitglied-id', 'source_type' => 'mitglieder'],
            ],
            'stufenverlauf' => [
                ['header' => 'mitglied-id', 'source_type' => 'mitglieder'],
                ['header' => 'stufe-id', 'source_type' => 'stufen'],
            ],
            'beitraege' => [
                ['header' => 'mitglied-id', 'source_type' => 'mitglieder'],
            ],
            'lagerteilnehmer' => [
                ['header' => 'lager-id', 'source_type' => 'lager'],
                ['header' => 'mitglied-id', 'source_type' => 'mitglieder'],
            ],
            'verletzungen' => [
                ['header' => 'mitglied-id', 'source_type' => 'mitglieder'],
                ['header' => 'lager-id', 'source_type' => 'lager'],
            ],
            default => [],
        };
    }

    /**
     * Collect ID mappings from import results.
     * Maps old CSV IDs to new database IDs.
     */
    private function collectIdMappings(string $type, string $originalContent, array $result, array &$idMap): void {
        if (!isset($idMap[$type])) {
            $idMap[$type] = [];
        }

        // Parse original content to get old IDs
        $lines = explode("\n", str_replace("\r\n", "\n", $originalContent));
        if (count($lines) < 2) return;

        // Remove BOM
        if (substr($lines[0], 0, 3) === "\xEF\xBB\xBF") {
            $lines[0] = substr($lines[0], 3);
        }

        $headers = str_getcsv($lines[0], ';');
        $headers = array_map('strtolower', array_map('trim', $headers));
        $idColIdx = array_search('id', $headers);
        if ($idColIdx === false) return;

        // Build row-to-oldId mapping
        $oldIds = [];
        for ($i = 1; $i < count($lines); $i++) {
            $line = trim($lines[$i]);
            if ($line === '') continue;
            $row = str_getcsv($line, ';');
            if (isset($row[$idColIdx]) && $row[$idColIdx] !== '') {
                $oldIds[$i - 1] = $row[$idColIdx]; // 0-indexed row
            }
        }

        // Match with created results
        $createdIdx = 0;
        foreach ($result['details'] as $detail) {
            if ($detail['status'] === 'created' && isset($detail['id'])) {
                $rowNum = $detail['row'] - 2; // Convert to 0-indexed
                if (isset($oldIds[$rowNum])) {
                    $idMap[$type][$oldIds[$rowNum]] = $detail['id'];
                }
            }
        }
    }
}
