<?php

namespace App\Extensions\Installed\Importer\Services;

use App\Enums\{Status as TicketStatus, Priorities as TicketPriority, Organize as TicketOrganize};
use App\Extensions\Installed\Importer\Models\ImportJob;
use App\Models\{Tickets, Responses, User, Departments};
use Illuminate\Support\Facades\{DB, Hash, Log, Validator};
use Illuminate\Support\Str;

/**
 * Chunked CSV Importer Service
 *
 * Handles importing large CSV files in chunks with:
 * - Resume capability via ImportJob tracking
 * - Memory-efficient streaming
 * - Progress tracking
 * - Error recovery
 * - Background processing support
 */
class ChunkedCsvImporter
{
    protected ImportJob $job;
    protected CsvImporter $csvImporter;
    protected int $chunkSize;

    /**
     * Constructor
     *
     * @param ImportJob $job The import job to process
     */
    public function __construct(ImportJob $job)
    {
        $this->job = $job;
        $this->chunkSize = $job->chunk_size ?? config('extensions.importer.chunk_size', 100);
    }

    /**
     * Create a new import job from CSV file
     *
     * @param string $filePath Path to CSV file
     * @param string $importType Type of import (customers, tickets, responses)
     * @param array $mapping Field mapping
     * @param array $options Import options
     * @param string $name Optional job name
     * @return ImportJob
     */
    public static function createJob(
        string $filePath,
        string $importType,
        array $mapping,
        array $options = [],
        ?string $name = null
    ): ImportJob {
        // Count total rows in CSV
        $totalRows = static::countCsvRows($filePath, $options['skip_header'] ?? true);

        return ImportJob::create([
            'name' => $name ?? "Import {$importType} from CSV",
            'source' => 'csv',
            'type' => $importType,
            'status' => 'pending',
            'total_rows' => $totalRows,
            'processed_rows' => 0,
            'failed_rows' => 0,
            'current_offset' => 0,
            'chunk_size' => $options['chunk_size'] ?? config('extensions.importer.chunk_size', 100),
            'file_path' => $filePath,
            'field_mapping' => $mapping,
            'options' => $options,
            'errors' => [],
        ]);
    }

    /**
     * Process the import job (all chunks or single chunk)
     *
     * @param bool $singleChunk Process only one chunk (for background processing)
     * @return array Results summary
     */
    public function process(bool $singleChunk = false): array
    {
        // Start the job if not already started
        if ($this->job->status === 'pending') {
            $this->job->start();
        }

        $file = fopen($this->job->file_path, 'r');
        if (!$file) {
            $this->job->fail("Could not open file: {$this->job->file_path}");
            return $this->job->only(['status', 'result_summary', 'errors']);
        }

        $options = $this->job->options ?? [];
        $delimiter = $options['delimiter'] ?? ',';
        $enclosure = $options['enclosure'] ?? '"';
        $escape = $options['escape'] ?? '\\';
        $skipHeader = $options['skip_header'] ?? true;

        // Read and skip header
        $headers = fgetcsv($file, 0, $delimiter, $enclosure, $escape);
        if (!$headers) {
            fclose($file);
            $this->job->fail("Could not read CSV headers");
            return $this->job->only(['status', 'result_summary', 'errors']);
        }

        try {
            do {
                $chunkResult = $this->processChunk($file, $headers, $options);

                if ($singleChunk) {
                    break; // Process only one chunk for queue jobs
                }
            } while ($chunkResult['hasMore'] && $this->job->status === 'processing');

            fclose($file);

            // Mark as completed if all chunks processed
            if (!$this->job->hasMoreToProcess() && $this->job->status === 'processing') {
                $this->job->complete();
            }

            return [
                'status' => $this->job->status,
                'progress' => $this->job->getProgressPercentage(),
                'processed' => $this->job->processed_rows,
                'failed' => $this->job->failed_rows,
                'total' => $this->job->total_rows,
                'errors' => $this->job->errors,
            ];
        } catch (\Exception $e) {
            fclose($file);
            Log::error("Chunked CSV Import failed: {$e->getMessage()}", [
                'job_id' => $this->job->id,
                'exception' => $e,
            ]);
            $this->job->fail($e->getMessage());
            throw $e;
        }
    }

    /**
     * Process a single chunk of rows
     *
     * @param resource $file File handle
     * @param array $headers CSV headers
     * @param array $options Import options
     * @return array Chunk processing results
     */
    protected function processChunk($file, array $headers, array $options): array
    {
        $delimiter = $options['delimiter'] ?? ',';
        $enclosure = $options['enclosure'] ?? '"';
        $escape = $options['escape'] ?? '\\';

        $processedInChunk = 0;
        $failedInChunk = 0;
        $chunkErrors = [];
        $rowNumber = $this->job->current_offset;

        // Skip to current offset
        if ($rowNumber > 0) {
            for ($i = 0; $i < $rowNumber; $i++) {
                if (fgetcsv($file, 0, $delimiter, $enclosure, $escape) === false) {
                    return ['hasMore' => false];
                }
            }
        }

        // Process chunk
        $count = 0;
        while ($count < $this->chunkSize && ($row = fgetcsv($file, 0, $delimiter, $enclosure, $escape)) !== false) {
            $rowNumber++;
            $count++;

            try {
                $data = array_combine($headers, $row);
                $mappedData = $this->mapData($data);

                // Route to appropriate import method
                switch ($this->job->type) {
                    case 'customers':
                        $this->importCustomer($mappedData, $rowNumber);
                        break;
                    case 'tickets':
                        $this->importTicket($mappedData, $rowNumber);
                        break;
                    case 'responses':
                        $this->importResponse($mappedData, $rowNumber);
                        break;
                    case 'departments':
                        $this->importDepartment($mappedData, $rowNumber);
                        break;
                    default:
                        throw new \Exception("Unsupported import type: {$this->job->type}");
                }

                $processedInChunk++;
            } catch (\Exception $e) {
                $failedInChunk++;
                $chunkErrors[] = [
                    'row' => $rowNumber,
                    'error' => $e->getMessage(),
                    'data' => $row,
                ];
                Log::warning("Import row {$rowNumber} failed: {$e->getMessage()}");
            }
        }

        // Update job progress
        $this->job->updateProgress($processedInChunk, $failedInChunk, $chunkErrors);

        return [
            'hasMore' => $count === $this->chunkSize,
            'processed' => $processedInChunk,
            'failed' => $failedInChunk,
        ];
    }

    /**
     * Map CSV data using job's field mapping
     *
     * @param array $data
     * @return array
     */
    protected function mapData(array $data): array
    {
        $mapped = [];
        foreach ($this->job->field_mapping as $csvColumn => $ticagaField) {
            if (isset($data[$csvColumn])) {
                $mapped[$ticagaField] = $data[$csvColumn];
            }
        }
        return $mapped;
    }

    /**
     * Import a customer row
     *
     * @param array $data Mapped customer data
     * @param int $rowNumber Current row number
     * @return void
     */
    protected function importCustomer(array $data, int $rowNumber): void
    {
        $validator = Validator::make($data, [
            'email' => 'required|email',
            'name' => 'required|string|max:255',
        ]);

        if ($validator->fails()) {
            throw new \Exception("Validation failed: " . implode(', ', $validator->errors()->all()));
        }

        $options = $this->job->options ?? [];
        $forcePasswordReset = $options['force_password_reset'] ?? true;
        if (array_key_exists('force_password_reset', $data)) {
            $forcePasswordReset = $this->interpretBoolean($data['force_password_reset']);
        }

        $temporaryPassword = trim((string)($data['password'] ?? ''));
        $passwordToUse = $temporaryPassword !== ''
            ? Hash::make($temporaryPassword)
            : Hash::make($options['default_password'] ?? 'changeme123');

        $accountManagerValue = $this->normalizeInteger($data['account_manager'] ?? 0);
        $billingIdValue = $this->normalizeInteger($data['billing_id'] ?? 0);
        $emailVerifiedAt = $data['email_verified_at'] ?? null;

        $existingUser = User::where('email', $data['email'])->first();

        if ($existingUser) {
            if ($options['update_existing'] ?? false) {
                $updatePayload = [
                    'name' => $data['name'],
                    'force_password_reset' => $forcePasswordReset,
                    'billing_id' => $billingIdValue,
                    'account_manager' => $accountManagerValue,
                ];

                if (array_key_exists('company', $data) && $data['company'] !== '') {
                    $updatePayload['company'] = $data['company'];
                }

                if (array_key_exists('billing_system', $data) && $data['billing_system'] !== '') {
                    $updatePayload['billing_system'] = $data['billing_system'];
                }

                if ($temporaryPassword !== '') {
                    $updatePayload['password'] = $passwordToUse;
                }

                if ($emailVerifiedAt !== null && $emailVerifiedAt !== '') {
                    $updatePayload['email_verified_at'] = $emailVerifiedAt;
                }

                $existingUser->update($updatePayload);

                if (method_exists($existingUser, 'assignRole') && method_exists($existingUser, 'hasRole')) {
                    if (!$existingUser->hasRole('customer')) {
                        $existingUser->assignRole('customer');
                    }
                }
            } else {
                throw new \Exception("Customer already exists: {$data['email']}");
            }
        } else {
            $createdUser = User::create([
                'name' => $data['name'],
                'email' => $data['email'],
                'company' => $data['company'] ?? null,
                'billing_id' => $billingIdValue,
                'billing_system' => $data['billing_system'] ?? null,
                'account_manager' => $accountManagerValue,
                'biography' => '',
                'password' => $passwordToUse,
                'email_verified_at' => $emailVerifiedAt,
                'force_password_reset' => $forcePasswordReset,
            ]);

            if (isset($createdUser) && method_exists($createdUser, 'assignRole')) {
                $createdUser->assignRole('customer');
            }
        }
    }

    /**
     * Import a ticket row
     *
     * @param array $data Mapped ticket data
     * @param int $rowNumber Current row number
     * @return void
     */
    protected function importTicket(array $data, int $rowNumber): void
    {
        $validator = Validator::make($data, [
            'subject' => 'required|string|max:255',
            'department_id' => 'nullable|integer|min:1',
            'department_name' => 'nullable|string',
            'user_id' => 'nullable|integer|min:0',
            'assigned' => 'nullable|integer|min:0',
            'public_email' => 'nullable|email',
            'ip_address' => 'nullable|string|max:45',
            'organize' => 'nullable|string',
        ]);

        $validator->after(function ($validator) use ($data) {
            $userId = isset($data['user_id']) ? (int) $data['user_id'] : 0;
            $publicEmail = $data['public_email'] ?? null;
            $userEmail = $data['user_email'] ?? null;
            $departmentId = isset($data['department_id']) ? (int) $data['department_id'] : 0;
            $departmentName = $data['department_name'] ?? null;

            if ($userId <= 0 && empty($publicEmail) && empty($userEmail)) {
                $validator->errors()->add(
                    'user_id',
                    'Either a valid user_id, user_email, or public_email is required for ticket imports.'
                );
            }

            if ($departmentId <= 0 && empty($departmentName)) {
                $validator->errors()->add(
                    'department_id',
                    'Either a valid department_id or department_name is required for ticket imports.'
                );
            }

            try {
                $this->normalizeStatusValue($data['status'] ?? null);
            } catch (\InvalidArgumentException $e) {
                $validator->errors()->add('status', $e->getMessage());
            }

            try {
                $this->normalizePriorityValue($data['priority'] ?? null);
            } catch (\InvalidArgumentException $e) {
                $validator->errors()->add('priority', $e->getMessage());
            }

            try {
                $this->normalizeOrganizeValue($data['organize'] ?? null);
            } catch (\InvalidArgumentException $e) {
                $validator->errors()->add('organize', $e->getMessage());
            }
        });

        if ($validator->fails()) {
            throw new \Exception("Validation failed: " . implode(', ', $validator->errors()->all()));
        }

        $user = null;
        $userId = isset($data['user_id']) ? (int) $data['user_id'] : 0;

        if ($userId > 0) {
            $user = User::find($userId);
            if (!$user) {
                throw new \Exception("Customer not found for user_id: {$userId}");
            }
        } elseif (!empty($data['user_email'])) {
            $user = User::where('email', $data['user_email'])->first();
            if (!$user) {
                throw new \Exception("Customer not found: {$data['user_email']}");
            }
        }

        $departmentId = isset($data['department_id']) ? (int) $data['department_id'] : 0;
        $departmentName = $data['department_name'] ?? null;

        if ($departmentId > 0) {
            $department = Departments::find($departmentId);
            if (!$department) {
                throw new \Exception("Department not found for department_id: {$departmentId}");
            }
        } else {
            $department = Departments::where('department_name', $departmentName)->first();
        }

        if (!$department) {
            $identifier = $departmentName ?? (string) $departmentId;
            throw new \Exception("Department not found: {$identifier}");
        }

        $publicEmail = $data['public_email'] ?? null;
        $statusValue = $this->normalizeStatusValue($data['status'] ?? null);
        $priorityValue = $this->normalizePriorityValue($data['priority'] ?? null);
        $organizeValue = $this->normalizeOrganizeValue($data['organize'] ?? null);

        $ticketPayload = [
            'subject' => $data['subject'],
            'message' => $data['message'] ?? '',
            'status' => $statusValue,
            'priority' => $priorityValue,
            'department_id' => $department->id,
            'public_hash' => hash_hmac('sha256', uniqid('', true), config('app.key')),
            'organize' => $organizeValue,
            'created_at' => $data['created_at'] ?? now(),
            'updated_at' => $data['updated_at'] ?? now(),
        ];

        if ($user) {
            $ticketPayload['user_id'] = $user->id;
        } else {
            $ticketPayload['user_id'] = null;
        }

        if (!empty($data['public_name'])) {
            $ticketPayload['public_name'] = $data['public_name'];
        }

        if ($publicEmail !== null) {
            $ticketPayload['public_email'] = $publicEmail;
        }

        if (array_key_exists('assigned', $data)) {
            $ticketPayload['assigned'] = (string) $this->normalizeInteger($data['assigned']);
        }

        if (array_key_exists('ip_address', $data)) {
            $ticketPayload['ip_address'] = $data['ip_address'] !== '' ? $data['ip_address'] : null;
        }

        Tickets::create($ticketPayload);
    }

    /**
     * Import a department row
     *
     * @param array $data Mapped department data
     * @param int $rowNumber Current row number
     * @return void
     */
    protected function importDepartment(array $data, int $rowNumber): void
    {
        $validator = Validator::make($data, [
            'department_name' => 'required|string|max:255',
            'slug' => 'nullable|string|max:255',
            'department_email' => 'nullable|email',
        ]);

        if ($validator->fails()) {
            throw new \Exception("Validation failed: " . implode(', ', $validator->errors()->all()));
        }

        $options = $this->job->options ?? [];
        $updateExisting = $options['update_existing'] ?? false;

        $slugSource = $data['slug'] ?? $data['department_name'];
        $normalizedSlug = Str::slug((string) $slugSource);
        if ($normalizedSlug === '') {
            $normalizedSlug = 'department';
        }

        $existingByName = Departments::where('department_name', $data['department_name'])->first();
        $existingBySlug = Departments::where('slug', $normalizedSlug)->first();

        $existingDepartment = $existingByName ?? ($updateExisting ? $existingBySlug : null);

        $departmentPayload = [
            'department_name' => $data['department_name'],
            'department_description' => $data['department_description'] ?? null,
            'department_email' => $data['department_email'] ?? null,
            'allows_high_priority' => array_key_exists('allows_high_priority', $data)
                ? $this->interpretBoolean($data['allows_high_priority'])
                : true,
            'cc_enabled' => array_key_exists('cc_enabled', $data)
                ? $this->interpretBoolean($data['cc_enabled'])
                : false,
            'is_public' => array_key_exists('is_public', $data)
                ? $this->interpretBoolean($data['is_public'])
                : true,
            'is_disabled' => array_key_exists('is_disabled', $data)
                ? $this->interpretBoolean($data['is_disabled'])
                : false,
            'soft_deleted' => array_key_exists('soft_deleted', $data)
                ? $this->interpretBoolean($data['soft_deleted'])
                : false,
            'created_at' => $data['created_at'] ?? now(),
            'updated_at' => $data['updated_at'] ?? now(),
        ];

        if ($existingDepartment) {
            if (!$updateExisting) {
                throw new \Exception("Department already exists: {$data['department_name']} ({$normalizedSlug})");
            }

            $departmentPayload['slug'] = $existingDepartment->slug;
            $existingDepartment->update($departmentPayload);
            return;
        }

        $departmentPayload['slug'] = $this->generateUniqueSlug($normalizedSlug);

        Departments::create($departmentPayload);
    }

    /**
     * Import a response row
     *
     * @param array $data Mapped response data
     * @param int $rowNumber Current row number
     * @return void
     */
    protected function importResponse(array $data, int $rowNumber): void
    {
        $validator = Validator::make($data, [
            'ticket_id' => 'nullable|exists:tickets,id',
            'ticket_number' => 'nullable|integer|min:1',
            'content' => 'required|string',
            'organize' => 'nullable|string',
            'ip_address' => 'nullable|string|max:255',
            'employee_response' => 'nullable',
        ]);

        $validator->after(function ($validator) use ($data) {
            $ticketId = $data['ticket_id'] ?? null;
            $ticketNumber = $data['ticket_number'] ?? null;

            if (empty($ticketId) && empty($ticketNumber)) {
                $validator->errors()->add('ticket_id', 'Either a valid ticket_id or ticket_number is required for response imports.');
            }
        });

        if ($validator->fails()) {
            throw new \Exception("Validation failed: " . implode(', ', $validator->errors()->all()));
        }

        $ticket = null;
        if (!empty($data['ticket_id'])) {
            $ticket = Tickets::find($data['ticket_id']);
        } elseif (!empty($data['ticket_number'])) {
            $ticket = Tickets::find($data['ticket_number']);
        }

        if (!$ticket) {
            $reference = $data['ticket_id'] ?? $data['ticket_number'];
            throw new \Exception("Ticket not found: {$reference}");
        }

        $userId = $data['user_id'] ?? $ticket->user_id;

        $organizeValue = $this->normalizeOrganizeValue($data['organize'] ?? 'ticket');
        $employeeResponse = array_key_exists('employee_response', $data)
            ? $this->interpretBoolean($data['employee_response'])
            : (array_key_exists('is_employee', $data)
                ? $this->interpretBoolean($data['is_employee'])
                : false);
        $isNote = array_key_exists('is_note', $data)
            ? $this->interpretBoolean($data['is_note'])
            : false;

        Responses::create([
            'ticket_number' => $ticket->id,
            'user_id' => $userId,
            'content' => $data['content'],
            'employee_response' => $employeeResponse,
            'is_note' => $isNote,
            'organize' => $organizeValue,
            'ip_address' => $data['ip_address'] ?? null,
            'created_at' => $data['created_at'] ?? now(),
            'updated_at' => $data['updated_at'] ?? now(),
        ]);
    }

    /**
     * Count total rows in CSV file
     *
     * @param string $filePath
     * @param bool $skipHeader
     * @return int
     */
    protected static function countCsvRows(string $filePath, bool $skipHeader = true): int
    {
        if (!file_exists($filePath)) {
            throw new \Exception("File not found: {$filePath}");
        }

        $file = fopen($filePath, 'r');
        $count = 0;

        while (fgets($file) !== false) {
            $count++;
        }

        fclose($file);

        // Subtract header row if needed
        return $skipHeader ? max(0, $count - 1) : $count;
    }

    /**
     * Pause the import job
     */
    public function pause(): void
    {
        $this->job->pause();
    }

    /**
     * Resume the import job
     */
    public function resume(): void
    {
        $this->job->resume();
    }

    /**
     * Get the import job
     *
     * @return ImportJob
     */
    public function getJob(): ImportJob
    {
        return $this->job->fresh();
    }

    protected function normalizeStatusValue($value): string
    {
        if ($value === null || $value === '') {
            return TicketStatus::Open->value;
        }

        if (is_numeric($value)) {
            $statusMap = [
                -1 => TicketStatus::Closed->value,
                0 => TicketStatus::Open->value,
                1 => TicketStatus::Open->value,
                2 => TicketStatus::InProgress->value,
                3 => TicketStatus::AwaitingReply->value,
                4 => TicketStatus::InProgress->value,
            ];

            $intValue = (int) $value;
            if (isset($statusMap[$intValue])) {
                return $statusMap[$intValue];
            }
        }

        $normalized = strtolower(trim((string) $value));
        $aliases = [
            TicketStatus::Open->value => TicketStatus::Open->value,
            'open' => TicketStatus::Open->value,
            'unassigned' => TicketStatus::Open->value,
            TicketStatus::Closed->value => TicketStatus::Closed->value,
            'closed' => TicketStatus::Closed->value,
            TicketStatus::AwaitingReply->value => TicketStatus::AwaitingReply->value,
            'awaiting_reply' => TicketStatus::AwaitingReply->value,
            'awaitingreply' => TicketStatus::AwaitingReply->value,
            'waiting on customer' => TicketStatus::AwaitingReply->value,
            'waiting_on_customer' => TicketStatus::AwaitingReply->value,
            'waitingoncustomer' => TicketStatus::AwaitingReply->value,
            TicketStatus::InProgress->value => TicketStatus::InProgress->value,
            'in_progress' => TicketStatus::InProgress->value,
            'inprogress' => TicketStatus::InProgress->value,
            'on_hold' => TicketStatus::InProgress->value,
            'on hold' => TicketStatus::InProgress->value,
            'hold' => TicketStatus::InProgress->value,
            'trash' => TicketStatus::Closed->value,
        ];

        if (isset($aliases[$normalized])) {
            return $aliases[$normalized];
        }

        throw new \InvalidArgumentException("Unsupported status value: {$value}");
    }

    protected function normalizePriorityValue($value): string
    {
        if ($value === null || $value === '') {
            return TicketPriority::None->value;
        }

        if (is_numeric($value)) {
            $priorityMap = [
                0 => TicketPriority::None->value,
                1 => TicketPriority::Low->value,
                2 => TicketPriority::Medium->value,
                3 => TicketPriority::High->value,
                4 => TicketPriority::Emergency->value,
            ];

            $intValue = (int) $value;
            if (isset($priorityMap[$intValue])) {
                return $priorityMap[$intValue];
            }
        }

        $normalized = strtolower(trim((string) $value));
        $aliases = [
            TicketPriority::None->value => TicketPriority::None->value,
            'not set' => TicketPriority::None->value,
            TicketPriority::Low->value => TicketPriority::Low->value,
            TicketPriority::Medium->value => TicketPriority::Medium->value,
            TicketPriority::High->value => TicketPriority::High->value,
            TicketPriority::Emergency->value => TicketPriority::Emergency->value,
            'urgent' => TicketPriority::Emergency->value,
        ];

        if (isset($aliases[$normalized])) {
            return $aliases[$normalized];
        }

        throw new \InvalidArgumentException("Unsupported priority value: {$value}");
    }

    protected function normalizeOrganizeValue($value): string
    {
        if ($value === null || $value === '') {
            return TicketOrganize::Ticket->value;
        }

        $normalized = strtolower(trim((string) $value));
        $aliases = [
            TicketOrganize::Ticket->value => TicketOrganize::Ticket->value,
            'web' => TicketOrganize::Ticket->value,
            TicketOrganize::Email->value => TicketOrganize::Email->value,
            'mail' => TicketOrganize::Email->value,
            'imap' => TicketOrganize::Email->value,
            TicketOrganize::Blesta->value => TicketOrganize::Blesta->value,
            TicketOrganize::WHMCS->value => TicketOrganize::WHMCS->value,
            'clientexec' => TicketOrganize::ClientExec->value,
            'client_exec' => TicketOrganize::ClientExec->value,
            'client-exec' => TicketOrganize::ClientExec->value,
        ];

        if (isset($aliases[$normalized])) {
            return $aliases[$normalized];
        }

        foreach (TicketOrganize::cases() as $case) {
            if (strcasecmp($case->value, (string) $value) === 0) {
                return $case->value;
            }
        }

        throw new \InvalidArgumentException("Unsupported organize value: {$value}");
    }

    protected function interpretBoolean($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        $normalized = strtolower(trim((string) $value));

        return in_array($normalized, ['1', 'true', 'yes', 'y', 'on', 'enabled'], true);
    }

    protected function normalizeInteger($value): int
    {
        if ($value === null || $value === '' || !is_numeric($value)) {
            return 0;
        }

        return (int) $value;
    }

    protected function generateUniqueSlug(string $baseSlug): string
    {
        $slug = $baseSlug;
        $counter = 1;

        while ($this->slugExists($slug)) {
            $slug = $baseSlug . '-' . $counter;
            $counter++;
        }

        return $slug;
    }

    protected function slugExists(string $slug): bool
    {
        return Departments::where('slug', $slug)->exists();
    }
}
