<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Service;

use DateTime;
use OCA\Mitgliederverwaltung\Db\Address;
use OCA\Mitgliederverwaltung\Db\AddressMapper;
use OCA\Mitgliederverwaltung\Db\Email;
use OCA\Mitgliederverwaltung\Db\EmailMapper;
use OCA\Mitgliederverwaltung\Db\Family;
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\Phone;
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
use OCA\Mitgliederverwaltung\Validation\IbanValidator;
use OCA\Mitgliederverwaltung\Validation\PhoneValidator;
use Psr\Log\LoggerInterface;

/**
 * Service for importing members from CSV files.
 *
 * Supports:
 * - CSV parsing with configurable delimiter and encoding
 * - Column-to-field mapping
 * - Validation (required fields, IBAN, phone E.164, date plausibility)
 * - Duplicate detection (Vorname + Nachname + Geburtsdatum)
 * - Family grouping (shared Nachname + address)
 * - Dry-run mode (preview without creating)
 * - Commit mode (create members, families, addresses, phones, emails)
 *
 * Part of Issue #58.
 */
class ImportService {

    /**
     * Required fields for member creation.
     */
    private const REQUIRED_FIELDS = ['vorname', 'nachname', 'eintritt'];

    /**
     * All available target fields with their labels.
     */
    public const TARGET_FIELDS = [
        'vorname' => 'Vorname',
        'nachname' => 'Nachname',
        'geburtsdatum' => 'Geburtsdatum',
        'geschlecht' => 'Geschlecht',
        'rolle' => 'Rolle',
        'eintritt' => 'Eintrittsdatum',
        'status' => 'Status',
        'notizen' => 'Notizen',
        'kv_typ' => 'KV Typ',
        'kv_name' => 'KV Name',
        'strasse' => 'Strasse',
        'plz' => 'PLZ',
        'ort' => 'Ort',
        'land' => 'Land',
        'telefon' => 'Telefon',
        'email' => 'E-Mail',
        'iban' => 'IBAN (verschluesselt)',
    ];

    private MemberMapper $memberMapper;
    private AddressMapper $addressMapper;
    private PhoneMapper $phoneMapper;
    private EmailMapper $emailMapper;
    private FamilyMapper $familyMapper;
    private AuditService $auditService;
    private LoggerInterface $logger;

    public function __construct(
        MemberMapper $memberMapper,
        AddressMapper $addressMapper,
        PhoneMapper $phoneMapper,
        EmailMapper $emailMapper,
        FamilyMapper $familyMapper,
        AuditService $auditService,
        LoggerInterface $logger
    ) {
        $this->memberMapper = $memberMapper;
        $this->addressMapper = $addressMapper;
        $this->phoneMapper = $phoneMapper;
        $this->emailMapper = $emailMapper;
        $this->familyMapper = $familyMapper;
        $this->auditService = $auditService;
        $this->logger = $logger;
    }

    // ── File parsing ────────────────────────────────────────────────

    /**
     * Parse a CSV file and return its headers and first N rows as preview.
     *
     * @param string $content Raw file content
     * @param string $delimiter CSV delimiter (default: ,)
     * @param string $encoding Source encoding (default: UTF-8)
     * @return array{columns: string[], rows: array[], totalRows: int}
     */
    public function parseFile(string $content, string $delimiter = ',', string $encoding = 'UTF-8'): array {
        // Convert encoding if necessary
        if (strtoupper($encoding) !== 'UTF-8') {
            $content = mb_convert_encoding($content, 'UTF-8', $encoding);
        }

        // Remove BOM if present
        $content = $this->removeBom($content);

        if (trim($content) === '') {
            throw new ValidationException('Datei ist leer');
        }

        $lines = str_getcsv($content, "\n");
        if (empty($lines)) {
            throw new ValidationException('Datei ist leer');
        }

        $rows = [];
        foreach ($lines as $line) {
            $row = str_getcsv($line, $delimiter);
            if ($row !== false && !$this->isEmptyRow($row)) {
                $rows[] = $row;
            }
        }

        if (count($rows) < 2) {
            throw new ValidationException('Datei muss mindestens eine Kopfzeile und eine Datenzeile enthalten');
        }

        $columns = array_shift($rows);
        // Trim column headers
        $columns = array_map('trim', $columns);

        return [
            'columns' => $columns,
            'rows' => array_slice($rows, 0, 50), // Preview max 50 rows
            'totalRows' => count($rows),
        ];
    }

    /**
     * Get available target fields for column mapping.
     *
     * @return array Target fields with keys and labels
     */
    public function getTargetFields(): array {
        $fields = [];
        foreach (self::TARGET_FIELDS as $key => $label) {
            $fields[] = [
                'key' => $key,
                'label' => $label,
                'required' => in_array($key, self::REQUIRED_FIELDS),
            ];
        }
        return $fields;
    }

    // ── Preview / Dry-run ───────────────────────────────────────────

    /**
     * Run import in dry-run mode: validate all rows and detect duplicates.
     *
     * @param string $content Raw CSV content
     * @param array $mapping Column mapping: { sourceIndex => targetField }
     * @param string $delimiter CSV delimiter
     * @param string $encoding Source encoding
     * @param bool $groupFamilies Whether to group by Nachname + address
     * @return array{valid: array[], errors: array[], duplicates: array[], summary: array}
     */
    public function preview(
        string $content,
        array $mapping,
        string $delimiter = ',',
        string $encoding = 'UTF-8',
        bool $groupFamilies = false
    ): array {
        $parsed = $this->parseFile($content, $delimiter, $encoding);
        $allRows = $this->parseAllRows($content, $delimiter, $encoding);

        $valid = [];
        $errors = [];
        $duplicates = [];

        foreach ($allRows as $rowIndex => $row) {
            $mapped = $this->applyMapping($row, $mapping);
            $rowErrors = $this->validateRow($mapped, $rowIndex + 2); // +2 for header + 0-index

            // Check for duplicates
            $isDuplicate = $this->detectDuplicate($mapped);

            if ($isDuplicate) {
                $mapped['_rowIndex'] = $rowIndex + 2;
                $mapped['_reason'] = 'Duplikat (Vorname + Nachname + Geburtsdatum bereits vorhanden)';
                $duplicates[] = $mapped;
            } elseif (!empty($rowErrors)) {
                $mapped['_rowIndex'] = $rowIndex + 2;
                $mapped['_errors'] = $rowErrors;
                $errors[] = $mapped;
            } else {
                $mapped['_rowIndex'] = $rowIndex + 2;
                $valid[] = $mapped;
            }
        }

        return [
            'valid' => $valid,
            'errors' => $errors,
            'duplicates' => $duplicates,
            'summary' => [
                'total' => count($allRows),
                'toCreate' => count($valid),
                'duplicates' => count($duplicates),
                'errors' => count($errors),
            ],
        ];
    }

    // ── Execute import ──────────────────────────────────────────────

    /**
     * Execute the import: create members, addresses, phones, emails.
     *
     * Only processes rows that passed validation (no errors, no duplicates).
     *
     * @param string $content Raw CSV content
     * @param array $mapping Column mapping
     * @param string $delimiter CSV delimiter
     * @param string $encoding Source encoding
     * @param bool $groupFamilies Whether to auto-create families
     * @return array{created: int, skipped: int, errors: int, details: array[]}
     */
    public function execute(
        string $content,
        array $mapping,
        string $delimiter = ',',
        string $encoding = 'UTF-8',
        bool $groupFamilies = false
    ): array {
        $allRows = $this->parseAllRows($content, $delimiter, $encoding);

        $created = 0;
        $skipped = 0;
        $errorCount = 0;
        $details = [];
        $familyCache = []; // Nachname+PLZ+Ort => familyId

        foreach ($allRows as $rowIndex => $row) {
            $mapped = $this->applyMapping($row, $mapping);
            $rowErrors = $this->validateRow($mapped, $rowIndex + 2);

            // Skip invalid rows
            if (!empty($rowErrors)) {
                $errorCount++;
                $details[] = [
                    'row' => $rowIndex + 2,
                    'status' => 'error',
                    'errors' => $rowErrors,
                ];
                continue;
            }

            // Skip duplicates
            if ($this->detectDuplicate($mapped)) {
                $skipped++;
                $details[] = [
                    'row' => $rowIndex + 2,
                    'status' => 'skipped',
                    'reason' => 'Duplikat',
                ];
                continue;
            }

            try {
                $now = (new DateTime())->format('Y-m-d H:i:s');

                // Create member
                $member = new Member();
                $member->setVorname(trim($mapped['vorname']));
                $member->setNachname(trim($mapped['nachname']));
                $member->setGeburtsdatum(!empty($mapped['geburtsdatum']) ? $this->normalizeDate($mapped['geburtsdatum']) : null);
                $member->setEintritt($this->normalizeDate($mapped['eintritt']));
                $member->setRolle($mapped['rolle'] ?? 'mitglied');
                $member->setStatus($mapped['status'] ?? 'aktiv');
                $member->setGeschlecht($mapped['geschlecht'] ?? null);
                $member->setNotizen($mapped['notizen'] ?? null);
                $member->setKvTyp($mapped['kv_typ'] ?? null);
                $member->setKvName($mapped['kv_name'] ?? null);
                $member->setCreatedAt($now);
                $member->setUpdatedAt($now);

                // Family grouping
                if ($groupFamilies && !empty($mapped['nachname']) && !empty($mapped['plz']) && !empty($mapped['ort'])) {
                    $familyKey = $mapped['nachname'] . '|' . $mapped['plz'] . '|' . $mapped['ort'];
                    if (isset($familyCache[$familyKey])) {
                        $member->setFamilyId($familyCache[$familyKey]);
                    } else {
                        $family = new Family();
                        $family->setName('Familie ' . $mapped['nachname']);
                        $family->setCreatedAt($now);
                        $family->setUpdatedAt($now);
                        $family = $this->familyMapper->insert($family);
                        $familyCache[$familyKey] = $family->getId();
                        $member->setFamilyId($family->getId());
                    }
                }

                /** @var Member $member */
                $member = $this->memberMapper->insert($member);
                $memberId = $member->getId();

                // Create address if provided
                if (!empty($mapped['strasse']) || !empty($mapped['plz']) || !empty($mapped['ort'])) {
                    $address = new Address();
                    $address->setMemberId($memberId);
                    $address->setStrasse($mapped['strasse'] ?? '');
                    $address->setPlz($mapped['plz'] ?? '');
                    $address->setOrt($mapped['ort'] ?? '');
                    $address->setLand($mapped['land'] ?? 'Deutschland');
                    $address->setIsPrimary(true);
                    $this->addressMapper->insert($address);
                }

                // Create phone if provided
                if (!empty($mapped['telefon'])) {
                    $phone = new Phone();
                    $phone->setMemberId($memberId);
                    $phone->setNumberE164(PhoneValidator::normalize($mapped['telefon']));
                    $phone->setLabel('Import');
                    $this->phoneMapper->insert($phone);
                }

                // Create email if provided
                if (!empty($mapped['email'])) {
                    $email = new Email();
                    $email->setMemberId($memberId);
                    $email->setEmail(trim($mapped['email']));
                    $email->setLabel('Import');
                    $this->emailMapper->insert($email);
                }

                $this->auditService->logCreate($member->jsonSerialize(), 'member', $memberId);

                $created++;
                $details[] = [
                    'row' => $rowIndex + 2,
                    'status' => 'created',
                    'memberId' => $memberId,
                    'name' => $mapped['vorname'] . ' ' . $mapped['nachname'],
                ];
            } catch (\Exception $e) {
                $errorCount++;
                $details[] = [
                    'row' => $rowIndex + 2,
                    'status' => 'error',
                    'errors' => [$e->getMessage()],
                ];
                $this->logger->error('Import row failed', [
                    'row' => $rowIndex + 2,
                    'exception' => $e,
                    'app' => 'mitgliederverwaltung',
                ]);
            }
        }

        $this->logger->info('Import completed', [
            'created' => $created,
            'skipped' => $skipped,
            'errors' => $errorCount,
            'app' => 'mitgliederverwaltung',
        ]);

        return [
            'created' => $created,
            'skipped' => $skipped,
            'errors' => $errorCount,
            'details' => $details,
        ];
    }

    // ── Validation ──────────────────────────────────────────────────

    /**
     * Validate a single mapped row.
     *
     * @param array $mapped The mapped row data
     * @param int $rowNum The row number for error reporting
     * @return string[] List of validation errors
     */
    private function validateRow(array $mapped, int $rowNum): array {
        $errors = [];

        // Required fields
        foreach (self::REQUIRED_FIELDS as $field) {
            if (empty($mapped[$field]) || trim((string)$mapped[$field]) === '') {
                $label = self::TARGET_FIELDS[$field] ?? $field;
                $errors[] = "Pflichtfeld '$label' fehlt (Zeile $rowNum)";
            }
        }

        // Date validation
        foreach (['geburtsdatum', 'eintritt'] as $dateField) {
            if (!empty($mapped[$dateField]) && !$this->isValidDate($mapped[$dateField])) {
                $errors[] = "Ungueltiges Datum fuer '$dateField': {$mapped[$dateField]} (Zeile $rowNum)";
            }
        }

        // Phone validation
        if (!empty($mapped['telefon'])) {
            $normalized = PhoneValidator::normalize($mapped['telefon']);
            if (!PhoneValidator::isValid($normalized)) {
                $errors[] = "Ungueltige Telefonnummer: {$mapped['telefon']} (Zeile $rowNum)";
            }
        }

        // IBAN validation
        if (!empty($mapped['iban'])) {
            if (!IbanValidator::isValid($mapped['iban'])) {
                $errors[] = "Ungueltige IBAN: {$mapped['iban']} (Zeile $rowNum)";
            }
        }

        // Email validation
        if (!empty($mapped['email']) && !filter_var($mapped['email'], FILTER_VALIDATE_EMAIL)) {
            $errors[] = "Ungueltige E-Mail: {$mapped['email']} (Zeile $rowNum)";
        }

        // Date plausibility
        if (!empty($mapped['geburtsdatum']) && $this->isValidDate($mapped['geburtsdatum'])) {
            $date = $this->normalizeDate($mapped['geburtsdatum']);
            $year = (int)substr($date, 0, 4);
            $currentYear = (int)date('Y');
            if ($year < 1900 || $year > $currentYear) {
                $errors[] = "Unplausibles Geburtsdatum: $date (Zeile $rowNum)";
            }
        }

        return $errors;
    }

    /**
     * Check if a member with same Vorname + Nachname + Geburtsdatum already exists.
     */
    private function detectDuplicate(array $mapped): bool {
        if (empty($mapped['vorname']) || empty($mapped['nachname']) || empty($mapped['geburtsdatum'])) {
            return false;
        }

        try {
            $existing = $this->memberMapper->findByNameAndBirthdate(
                trim($mapped['vorname']),
                trim($mapped['nachname']),
                $this->normalizeDate($mapped['geburtsdatum'])
            );
            return $existing !== null;
        } catch (\Exception $e) {
            return false;
        }
    }

    // ── Helpers ──────────────────────────────────────────────────────

    /**
     * Parse all rows from CSV content (not just preview).
     */
    private function parseAllRows(string $content, string $delimiter, string $encoding): array {
        if (strtoupper($encoding) !== 'UTF-8') {
            $content = mb_convert_encoding($content, 'UTF-8', $encoding);
        }
        $content = $this->removeBom($content);

        $lines = str_getcsv($content, "\n");
        $rows = [];
        foreach ($lines as $line) {
            $row = str_getcsv($line, $delimiter);
            if ($row !== false && !$this->isEmptyRow($row)) {
                $rows[] = $row;
            }
        }

        // Remove header row
        array_shift($rows);
        return $rows;
    }

    /**
     * Apply column mapping to a row.
     *
     * @param array $row Raw CSV row
     * @param array $mapping { sourceColumnIndex => targetFieldKey }
     * @return array Mapped data with target field keys
     */
    private function applyMapping(array $row, array $mapping): array {
        $mapped = [];
        foreach ($mapping as $sourceIndex => $targetField) {
            if ($targetField !== '' && $targetField !== null && isset($row[(int)$sourceIndex])) {
                $mapped[$targetField] = trim($row[(int)$sourceIndex]);
            }
        }
        return $mapped;
    }

    /**
     * Check if a date string is valid (supports multiple formats).
     */
    private function isValidDate(string $date): bool {
        $formats = ['Y-m-d', 'd.m.Y', 'd/m/Y', 'm/d/Y'];
        foreach ($formats as $format) {
            $parsed = DateTime::createFromFormat($format, trim($date));
            if ($parsed !== false && $parsed->format($format) === trim($date)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Normalize a date string to Y-m-d format.
     */
    private function normalizeDate(string $date): string {
        $date = trim($date);

        // Already Y-m-d
        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            return $date;
        }

        // d.m.Y (German format)
        $parsed = DateTime::createFromFormat('d.m.Y', $date);
        if ($parsed !== false) {
            return $parsed->format('Y-m-d');
        }

        // d/m/Y
        $parsed = DateTime::createFromFormat('d/m/Y', $date);
        if ($parsed !== false) {
            return $parsed->format('Y-m-d');
        }

        return $date; // Return as-is if no format matches
    }

    /**
     * Remove UTF-8 BOM from content.
     */
    private function removeBom(string $content): string {
        if (substr($content, 0, 3) === "\xEF\xBB\xBF") {
            return substr($content, 3);
        }
        return $content;
    }

    /**
     * Check if a CSV row is completely empty.
     */
    private function isEmptyRow(array $row): bool {
        foreach ($row as $cell) {
            if ($cell !== null && trim($cell) !== '') {
                return false;
            }
        }
        return true;
    }
}
