<?php
namespace Domain\Tasks;

class MailToTaskImporter
{
  private const ALLOWED_SENDER = 'jacek.pyziak@project-pro.pl';
  private const TASK_USER_ID = 1;
  private const TASK_STATUS_ID = 5;
  private const TASK_PROJECT_ID = 72;

  private $mdb;
  private $attachments;
  private $last_ai_error;

  public function __construct( $mdb = null )
  {
    if ( $mdb )
      $this -> mdb = $mdb;
    else
    {
      global $mdb;
      $this -> mdb = $mdb;
    }

    $this -> attachments = new TaskAttachmentRepository( $this -> mdb );
    $this -> last_ai_error = '';
  }

  public function importFromImap( array $config )
  {
    if ( !extension_loaded( 'imap' ) )
      return [ 'status' => 'error', 'msg' => 'Brak rozszerzenia IMAP w PHP.' ];

    $mailbox = $this -> buildMailbox( $config );
    $username = isset( $config['username'] ) ? (string)$config['username'] : '';
    $password = isset( $config['password'] ) ? (string)$config['password'] : '';

    if ( $mailbox === '' or $username === '' or $password === '' )
      return [ 'status' => 'error', 'msg' => 'Brak konfiguracji skrzynki IMAP.' ];

    $imap = @imap_open( $mailbox, $username, $password );
    if ( !$imap )
      return [ 'status' => 'error', 'msg' => 'Nie udalo sie polaczyc z IMAP: ' . imap_last_error() ];

    $this -> ensureImportTable();

    $message_numbers = imap_search( $imap, 'ALL' );
    if ( !is_array( $message_numbers ) or !count( $message_numbers ) )
    {
      imap_close( $imap );
      return [ 'status' => 'empty', 'msg' => 'Brak wiadomosci do importu.' ];
    }

    sort( $message_numbers, SORT_NUMERIC );

    $imported = 0;
    $skipped = 0;
    $errors = 0;
    $first_error_msg = null;

    foreach ( $message_numbers as $message_no )
    {
      $header = @imap_headerinfo( $imap, $message_no );
      if ( !$header )
      {
        $errors++;
        continue;
      }

      $sender = $this -> extractSender( $header );
      $subject = $this -> decodeHeaderValue( isset( $header -> subject ) ? $header -> subject : '' );
      $received_at = $this -> parseReceivedDate( isset( $header -> date ) ? $header -> date : '' );
      $message_key = $this -> messageKey( $header, $sender, $subject, $message_no );

      if ( $this -> isMessageFinalized( $message_key ) )
      {
        $skipped++;
        @imap_delete( $imap, $message_no );
        continue;
      }

      if ( $sender !== self::ALLOWED_SENDER )
      {
        $this -> saveImportLog( $message_key, null, $sender, $subject, 'skipped_sender', null );
        $skipped++;
        @imap_delete( $imap, $message_no );
        continue;
      }

      try
      {
        $content = $this -> extractMessageContent( $imap, $message_no );
        $ai_used = false;
        $ai_error_for_log = null;

        // Sprawdź czy użyć AI do parsowania
        global $settings;
        $use_ai = isset( $settings['openai_parse_emails'] ) && $settings['openai_parse_emails'] === true;
        $api_key = isset( $settings['openai_api_key'] ) ? trim( (string)$settings['openai_api_key'] ) : '';
        if ( $use_ai && $api_key === '' )
        {
          $ai_error_for_log = 'Brak klucza API OpenAI.';
          error_log( '[MailToTaskImporter] AI fallback: ' . $ai_error_for_log );
        }

        if ( $use_ai && $api_key !== '' )
        {
          $model = isset( $settings['openai_model'] ) ? trim( (string)$settings['openai_model'] ) : 'gpt-4o-mini';
          $ai_result = $this -> parseWithAI( $api_key, $model, $subject, $content['text'] );
          if ( is_array( $ai_result ) && isset( $ai_result['task_name'] ) && isset( $ai_result['task_text'] ) )
          {
            $task_name = $ai_result['task_name'];
            $task_text = $ai_result['task_text'];
            $ai_used = true;
          }
          else
          {
            // Fallback do normalnego parsowania jeśli AI nie zadziała
            if ( $this -> last_ai_error !== '' )
            {
              error_log( '[MailToTaskImporter] AI fallback: ' . $this -> last_ai_error );
              $ai_error_for_log = $this -> last_ai_error;
            }

            $task_name = trim( $subject ) !== '' ? trim( $subject ) : '(bez tematu)';
            $task_text = $this -> prepareImportedTaskText( $content['text'] );
          }
        }
        else
        {
          // Normalne parsowanie
          $task_name = trim( $subject ) !== '' ? trim( $subject ) : '(bez tematu)';
          $task_text = $this -> prepareImportedTaskText( $content['text'] );
        }
        $client_id = $this -> resolveClientIdBySenderDomain( $sender );

        $task_id = \factory\Tasks::task_save(
          null,
          null,
          null,
          self::TASK_USER_ID,
          $task_name,
          $task_text,
          $received_at,
          $received_at,
          self::TASK_PROJECT_ID,
          $client_id,
          null,
          null,
          'off',
          0,
          0,
          [ self::TASK_USER_ID ],
          null,
          null,
          false,
          'off',
          true,
          self::TASK_STATUS_ID,
          'on',
          0
        );

        if ( !$task_id )
        {
          $db_error = $this -> mdb -> error();
          $db_error_text = is_array( $db_error ) ? implode( ' | ', array_filter( $db_error ) ) : '';
          $error_msg = 'Nie udalo sie utworzyc zadania.' . ( $db_error_text !== '' ? ' DB: ' . $db_error_text : '' );
          $this -> saveImportLog( $message_key, null, $sender, $subject, 'error', $error_msg );
          if ( $first_error_msg === null )
            $first_error_msg = $error_msg;
          $errors++;
          continue;
        }

        // Zapisz inline images jako załączniki i zbierz URLe
        $cid_to_url = [];
        if ( isset( $content['inline_images'] ) && is_array( $content['inline_images'] ) )
        {
          foreach ( $content['inline_images'] as $cid => $inline_image )
          {
            $img_name = trim( $inline_image['name'] );
            if ( $img_name === '' )
              $img_name = 'inline_' . md5( $cid ) . '.' . $this -> guessExtensionFromMime( $inline_image['mime'] );

            $result = $this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $img_name, $inline_image['content'] );
            if ( is_array( $result ) && $result['status'] === 'success' )
              $cid_to_url[$cid] = '/upload/task_attachments/' . $result['relative_path'] . '/' . $result['stored_name'];
          }
        }

        // Jeśli jest HTML, zastąp CIDy na URLe załączników i wyczyść style
        if ( isset( $content['html'] ) && trim( $content['html'] ) !== '' )
        {
          $html_clean = $content['html'];
          if ( !empty( $cid_to_url ) )
            $html_clean = $this -> replaceCidReferences( $html_clean, $cid_to_url );

          $task_text = $this -> prepareImportedTaskTextFromHtml( $html_clean );

          // Zaktualizuj treść zadania
          $this -> mdb -> update( 'tasks', [ 'text' => $task_text ], [ 'id' => $task_id ] );
        }

        // Zapisz normalne załączniki
        foreach ( $content['attachments'] as $attachment )
        {
          $att_name = self::normalizeImportedAttachmentName(
            isset( $attachment['name'] ) ? $attachment['name'] : '',
            isset( $attachment['mime'] ) ? $attachment['mime'] : '',
            isset( $attachment['content'] ) ? $attachment['content'] : null
          );
          $att_mime = isset( $attachment['mime'] ) ? $attachment['mime'] : '';

          // Jeśli nazwa nie ma rozszerzenia, dodaj je z MIME type
          if ( pathinfo( $att_name, PATHINFO_EXTENSION ) === '' && $att_mime !== '' )
          {
            $mime_ext = $this -> extensionFromMime( $att_mime );
            if ( $mime_ext !== '' )
              $att_name .= '.' . $mime_ext;
          }

          $this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $att_name, $attachment['content'] );
        }

        $import_status = 'imported';
        $import_error = null;
        if ( $use_ai && $ai_used )
          $import_status = 'imported_ai';
        elseif ( $use_ai && !$ai_used )
        {
          $import_status = 'imported_fallback';
          $import_error = $ai_error_for_log;
        }

        $this -> saveImportLog( $message_key, $task_id, $sender, $subject, $import_status, $import_error );
        $imported++;
      }
      catch ( \Throwable $e )
      {
        $this -> saveImportLog( $message_key, null, $sender, $subject, 'error', $e -> getMessage() );
        if ( $first_error_msg === null )
          $first_error_msg = $e -> getMessage();
        $errors++;
      }

      $status_tmp = $this -> getImportStatus( $message_key );
      if ( in_array( $status_tmp, [ 'imported', 'imported_ai', 'imported_fallback', 'skipped_sender' ], true ) )
        @imap_delete( $imap, $message_no );
    }

    @imap_expunge( $imap );
    @imap_close( $imap );

    if ( $imported > 0 )
      return [ 'status' => 'ok', 'msg' => 'Zaimportowano: ' . $imported . ', pominieto: ' . $skipped . ', bledy: ' . $errors . '.' ];

    if ( $errors > 0 )
    {
      $msg = 'Brak nowych zadan. Bledy: ' . $errors . '.';
      if ( $first_error_msg )
        $msg .= ' Ostatni blad: ' . $first_error_msg;
      return [ 'status' => 'error', 'msg' => $msg ];
    }

    return [ 'status' => 'empty', 'msg' => 'Brak nowych zadan. Pominieto: ' . $skipped . '.' ];
  }

  private function buildMailbox( array $config )
  {
    $mailbox = isset( $config['mailbox'] ) ? trim( (string)$config['mailbox'] ) : '';
    if ( $mailbox !== '' )
      return $mailbox;

    $host = isset( $config['host'] ) ? trim( (string)$config['host'] ) : '';
    if ( $host === '' )
      return '';

    $port = isset( $config['port'] ) ? (int)$config['port'] : 993;
    $flags = isset( $config['flags'] ) ? trim( (string)$config['flags'] ) : '/imap/ssl';
    $folder = isset( $config['folder'] ) ? trim( (string)$config['folder'] ) : 'INBOX';

    return '{' . $host . ':' . $port . $flags . '}' . $folder;
  }

  private function resolveClientIdBySenderDomain( $sender )
  {
    $domain = $this -> extractDomainFromEmail( $sender );
    if ( $domain === '' )
      return null;

    $rows = $this -> mdb -> select( 'crm_client', [ 'id', 'emails' ], [
      'emails[!]' => null
    ] );

    if ( !is_array( $rows ) or !count( $rows ) )
      return null;

    foreach ( $rows as $row )
    {
      $emails = $this -> parseEmailsField( isset( $row['emails'] ) ? $row['emails'] : '' );
      foreach ( $emails as $email )
      {
        $email_domain = $this -> extractDomainFromEmail( $email );
        if ( $email_domain !== '' and $email_domain === $domain )
          return (int)$row['id'];
      }
    }

    return null;
  }

  private function parseEmailsField( $emails_raw )
  {
    $emails_raw = (string)$emails_raw;
    if ( trim( $emails_raw ) === '' )
      return [];

    preg_match_all( '/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i', $emails_raw, $matches );
    if ( isset( $matches[0] ) and is_array( $matches[0] ) and count( $matches[0] ) )
      return array_values( array_unique( array_map( 'strtolower', $matches[0] ) ) );

    return [];
  }

  private function extractDomainFromEmail( $email )
  {
    $email = strtolower( trim( (string)$email ) );
    if ( $email === '' or strpos( $email, '@' ) === false )
      return '';

    $parts = explode( '@', $email );
    $domain = trim( end( $parts ) );
    return $domain !== '' ? $domain : '';
  }

  private function parseReceivedDate( $value )
  {
    $timestamp = strtotime( (string)$value );
    if ( $timestamp === false )
      return date( 'Y-m-d' );

    return date( 'Y-m-d', $timestamp );
  }

  private function extractSender( $header )
  {
    if ( !isset( $header -> from ) or !is_array( $header -> from ) or !isset( $header -> from[0] ) )
      return '';

    $item = $header -> from[0];
    $mailbox = isset( $item -> mailbox ) ? strtolower( trim( $item -> mailbox ) ) : '';
    $host = isset( $item -> host ) ? strtolower( trim( $item -> host ) ) : '';

    if ( $mailbox === '' or $host === '' )
      return '';

    return $mailbox . '@' . $host;
  }

  private function decodeHeaderValue( $value )
  {
    $value = (string)$value;
    if ( $value === '' )
      return '';

    if ( !function_exists( 'imap_mime_header_decode' ) )
      return trim( $this -> convertTextToUtf8( $value ) );

    $chunks = @imap_mime_header_decode( $value );
    if ( !is_array( $chunks ) or !count( $chunks ) )
      return trim( $this -> convertTextToUtf8( $value ) );

    $decoded = '';
    foreach ( $chunks as $chunk )
    {
      $chunk_text = isset( $chunk -> text ) ? (string)$chunk -> text : '';
      $chunk_charset = isset( $chunk -> charset ) ? (string)$chunk -> charset : '';
      $decoded .= $this -> convertTextToUtf8( $chunk_text, $chunk_charset );
    }

    return trim( $decoded );
  }

  private function convertTextToUtf8( $text, $source_charset = '' )
  {
    $text = (string)$text;
    if ( $text === '' )
      return '';

    if ( preg_match( '//u', $text ) )
      return $text;

    $source_charset = strtoupper( trim( (string)$source_charset ) );
    $aliases = [
      'DEFAULT' => '',
      'UNKNOWN-8BIT' => '',
      'X-UNKNOWN' => '',
      'UTF8' => 'UTF-8',
      'UTF-8' => 'UTF-8',
      'ISO-8859-2' => 'ISO-8859-2',
      'ISO8859-2' => 'ISO-8859-2',
      'WINDOWS-1250' => 'Windows-1250',
      'CP1250' => 'Windows-1250',
      'WINDOWS-1252' => 'Windows-1252',
      'CP1252' => 'Windows-1252'
    ];

    $candidates = [];
    if ( isset( $aliases[$source_charset] ) && $aliases[$source_charset] !== '' )
      $candidates[] = $aliases[$source_charset];

    $candidates = array_merge( $candidates, [ 'Windows-1250', 'ISO-8859-2', 'Windows-1252', 'ISO-8859-1' ] );
    $candidates = array_values( array_unique( $candidates ) );

    foreach ( $candidates as $encoding )
    {
      if ( function_exists( 'mb_convert_encoding' ) )
      {
        $converted = @mb_convert_encoding( $text, 'UTF-8', $encoding );
        if ( is_string( $converted ) && $converted !== '' && preg_match( '//u', $converted ) )
          return $converted;
      }

      if ( function_exists( 'iconv' ) )
      {
        $converted = @iconv( $encoding, 'UTF-8//IGNORE', $text );
        if ( is_string( $converted ) && $converted !== '' && preg_match( '//u', $converted ) )
          return $converted;
      }
    }

    if ( function_exists( 'mb_convert_encoding' ) )
    {
      $converted = @mb_convert_encoding( $text, 'UTF-8', 'UTF-8' );
      if ( is_string( $converted ) && $converted !== '' )
        return $converted;
    }

    return @iconv( 'UTF-8', 'UTF-8//IGNORE', $text ) ?: $text;
  }

  private function messageKey( $header, $sender, $subject, $message_no )
  {
    $message_id = '';
    if ( isset( $header -> message_id ) and trim( (string)$header -> message_id ) !== '' )
      $message_id = trim( (string)$header -> message_id );

    if ( $message_id === '' )
      $message_id = sha1( $sender . '|' . $subject . '|' . ( isset( $header -> date ) ? $header -> date : '' ) . '|' . $message_no );

    return $message_id;
  }

  private function isMessageFinalized( $message_key )
  {
    $status = $this -> getImportStatus( $message_key );
    return in_array( $status, [ 'imported', 'imported_ai', 'imported_fallback', 'skipped_sender' ], true );
  }

  private function getImportStatus( $message_key )
  {
    return $this -> mdb -> get( 'tasks_mail_import', 'status', [ 'message_key' => (string)$message_key ] );
  }

  private function saveImportLog( $message_key, $task_id, $sender, $subject, $status, $error )
  {
    $data = [
      'task_id' => $task_id ? (int)$task_id : null,
      'sender' => substr( (string)$sender, 0, 255 ),
      'subject' => substr( (string)$subject, 0, 255 ),
      'status' => substr( (string)$status, 0, 40 ),
      'error_msg' => $error !== null ? (string)$error : null,
      'date_add' => date( 'Y-m-d H:i:s' )
    ];

    if ( $this -> mdb -> count( 'tasks_mail_import', [ 'message_key' => (string)$message_key ] ) )
      $this -> mdb -> update( 'tasks_mail_import', $data, [ 'message_key' => (string)$message_key ] );
    else
      $this -> mdb -> insert( 'tasks_mail_import', array_merge( [ 'message_key' => (string)$message_key ], $data ) );
  }

  private function ensureImportTable()
  {
    $this -> mdb -> query(
      'CREATE TABLE IF NOT EXISTS `tasks_mail_import` (
        `id` INT NOT NULL AUTO_INCREMENT,
        `message_key` VARCHAR(255) NOT NULL,
        `task_id` INT NULL,
        `sender` VARCHAR(255) NULL,
        `subject` VARCHAR(255) NULL,
        `status` VARCHAR(40) NOT NULL,
        `error_msg` TEXT NULL,
        `date_add` DATETIME NOT NULL,
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_tasks_mail_import_message_key` (`message_key`),
        INDEX `idx_tasks_mail_import_task` (`task_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8'
    );
  }

  private function extractMessageContent( $imap, $message_no )
  {
    $structure = @imap_fetchstructure( $imap, $message_no );

    $parts = [];
    $this -> flattenParts( $imap, $message_no, $structure, '', $parts, [] );

    $plain = '';
    $html = '';
    $attachment_candidates = [];

    foreach ( $parts as $part )
    {
      if ( $part['mime'] === 'text/plain' and $plain === '' )
        $plain = $part['body'];

      if ( $part['mime'] === 'text/html' and $html === '' )
        $html = $part['body'];

      if ( $part['is_attachment'] )
        $attachment_candidates[] = $part;
    }

    $cid_refs = $this -> extractReferencedCidValues( $html );

    $attachments = [];
    $inline_images = [];

    foreach ( $attachment_candidates as $part )
    {
      $content_id = isset( $part['content_id'] ) ? (string)$part['content_id'] : '';

      // Sprawdź czy to inline image używany w HTML
      if ( $content_id !== '' && isset( $cid_refs[$content_id] ) )
      {
        $inline_images[$content_id] = [
          'name' => $part['name'],
          'content' => $part['body'],
          'mime' => isset( $part['mime'] ) ? $part['mime'] : 'application/octet-stream'
        ];
        continue;
      }

      $should_import = $this -> shouldImportAttachment( $part, $cid_refs );

      if ( !$should_import )
        continue;

      $attachments[] = [
        'name' => $part['name'],
        'content' => $part['body'],
        'mime' => isset( $part['mime'] ) ? $part['mime'] : 'application/octet-stream'
      ];
    }

    $text = $plain !== '' ? $plain : $this -> htmlToText( $html );
    $text = $this -> cleanBodyText( $text );

    return [
      'text' => $text,
      'html' => $html,
      'inline_images' => $inline_images,
      'attachments' => $attachments
    ];
  }

  private function flattenParts( $imap, $message_no, $structure, $prefix, array &$parts, array $parent_context = [] )
  {
    if ( !$structure )
    {
      $parts[] = [
        'mime' => 'text/plain',
        'body' => '',
        'name' => '',
        'is_attachment' => false,
        'is_inline' => false
      ];
      return;
    }

    if ( !isset( $structure -> parts ) or !is_array( $structure -> parts ) or !count( $structure -> parts ) )
    {
      $part_number = $prefix !== '' ? $prefix : null;
      $parts[] = $this -> parseSinglePart( $imap, $message_no, $structure, $part_number, $parent_context );
      return;
    }

    $current_context = $this -> mergePartContexts( $parent_context, $this -> extractPartContext( $structure ) );

    foreach ( $structure -> parts as $index => $subpart )
    {
      $part_number = $prefix === '' ? (string)( $index + 1 ) : $prefix . '.' . ( $index + 1 );
      $child_context = $this -> mergePartContexts( $current_context, $this -> extractPartContext( $subpart ) );
      if ( isset( $subpart -> parts ) and is_array( $subpart -> parts ) and count( $subpart -> parts ) )
        $this -> flattenParts( $imap, $message_no, $subpart, $part_number, $parts, $child_context );
      else
        $parts[] = $this -> parseSinglePart( $imap, $message_no, $subpart, $part_number, $child_context );
    }
  }

  private function parseSinglePart( $imap, $message_no, $part, $part_number, array $context = [] )
  {
    $raw = '';
    if ( $part_number === null )
      $raw = (string)@imap_body( $imap, $message_no, FT_PEEK );
    else
      $raw = (string)@imap_fetchbody( $imap, $message_no, $part_number, FT_PEEK );

    $mime = $this -> mimeType( $part );
    $decoded = $this -> decodePartBody( $raw, isset( $part -> encoding ) ? (int)$part -> encoding : 0 );
    if ( strpos( $mime, 'text/' ) === 0 )
      $decoded = str_replace( [ "\r\n", "\r" ], "\n", (string)$decoded );

    $params = $this -> partParams( $part );

    $filename_param = isset( $params['filename'] ) ? $this -> decodeHeaderValue( $params['filename'] ) : '';
    $name_param = isset( $params['name'] ) ? $this -> decodeHeaderValue( $params['name'] ) : '';
    $description_param = isset( $params['description'] ) ? $this -> decodeHeaderValue( $params['description'] ) : '';
    $name = $this -> pickBetterAttachmentName( $filename_param, $name_param );
    $name = $this -> pickBetterAttachmentName( $name, $description_param );

    if ( isset( $part -> description ) && trim( (string)$part -> description ) !== '' )
      $name = $this -> pickBetterAttachmentName( $name, $this -> decodeHeaderValue( (string)$part -> description ) );

    if ( isset( $context['name'] ) && trim( (string)$context['name'] ) !== '' )
      $name = $this -> pickBetterAttachmentName( $name, (string)$context['name'] );

    // Fallback: jeśli brak nazwy, brak rozszerzenia lub nazwa techniczna, spróbuj z surowych nagłówków MIME
    if ( $part_number !== null && ( trim( $name ) === '' || pathinfo( $name, PATHINFO_EXTENSION ) === '' || self::isTechnicalAttachmentName( $name ) ) )
    {
      $mime_name = $this -> extractNameFromRawMime( $imap, $message_no, $part_number );
      if ( $mime_name !== '' )
        $name = $mime_name;
    }

    $name = self::normalizeImportedAttachmentName( $name, $mime, $decoded );

    $disposition = isset( $part -> disposition ) ? strtoupper( trim( (string)$part -> disposition ) ) : '';
    if ( $disposition === '' && isset( $context['disposition'] ) )
      $disposition = strtoupper( trim( (string)$context['disposition'] ) );

    $content_id = '';
    if ( isset( $part -> id ) and trim( (string)$part -> id ) !== '' )
      $content_id = $this -> normalizeContentId( (string)$part -> id );
    elseif ( isset( $params['content-id'] ) and trim( (string)$params['content-id'] ) !== '' )
      $content_id = $this -> normalizeContentId( (string)$params['content-id'] );
    elseif ( isset( $context['content_id'] ) )
      $content_id = $this -> normalizeContentId( (string)$context['content_id'] );

    $is_inline = $disposition === 'INLINE' or $content_id !== '';
    $is_attachment = trim( $name ) !== ''
      or $disposition === 'ATTACHMENT'
      or $this -> isLikelyBinaryAttachmentWithoutName( $mime, $decoded, $disposition, $content_id );

    return [
      'mime' => $mime,
      'body' => $decoded,
      'name' => $name,
      'is_attachment' => $is_attachment,
      'is_inline' => $is_inline,
      'disposition' => $disposition,
      'content_id' => $content_id
    ];
  }

  private function partParams( $part )
  {
    $params = [];

    if ( isset( $part -> parameters ) and is_array( $part -> parameters ) )
    {
      foreach ( $part -> parameters as $param )
      {
        if ( isset( $param -> attribute ) )
          $params[ strtolower( (string)$param -> attribute ) ] = isset( $param -> value ) ? (string)$param -> value : '';
      }
    }

    if ( isset( $part -> dparameters ) and is_array( $part -> dparameters ) )
    {
      foreach ( $part -> dparameters as $param )
      {
        if ( isset( $param -> attribute ) )
          $params[ strtolower( (string)$param -> attribute ) ] = isset( $param -> value ) ? (string)$param -> value : '';
      }
    }

    return $this -> reassembleRfc2231Params( $params );
  }

  private function reassembleRfc2231Params( array $params )
  {
    $normal = [];
    $rfc2231 = [];

    foreach ( $params as $key => $value )
    {
      if ( preg_match( '/^([^*]+)\*(\d+)?\*?$/', $key, $m ) )
      {
        $base = $m[1];
        $index = isset( $m[2] ) && $m[2] !== '' ? (int)$m[2] : 0;
        $rfc2231[$base][$index] = $value;
      }
      else
        $normal[$key] = $value;
    }

    foreach ( $rfc2231 as $base => $parts )
    {
      ksort( $parts, SORT_NUMERIC );
      $combined = implode( '', $parts );

      if ( preg_match( "/^([^']*)'([^']*)'(.+)$/s", $combined, $enc ) )
      {
        $charset = strtoupper( $enc[1] );
        $decoded = rawurldecode( $enc[3] );
        if ( $charset !== '' && $charset !== 'UTF-8' && function_exists( 'mb_convert_encoding' ) )
          $decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset );
        $combined = $decoded;
      }

      if ( $combined !== '' || !isset( $normal[$base] ) )
        $normal[$base] = $combined;
    }

    return $normal;
  }

  private function extractNameFromRawMime( $imap, $message_no, $part_number )
  {
    $headers = $this -> fetchRawMimeHeaders( $imap, $message_no, $part_number );

    if ( trim( $headers ) === '' )
      return '';

    return $this -> extractNameFromMimeHeaders( $headers );
  }

  private function extractNameFromMimeHeaders( $headers )
  {
    $headers = (string)$headers;
    if ( trim( $headers ) === '' )
      return '';

    $best_name = '';

    // Szukaj filename w Content-Disposition, potem name w Content-Type
    foreach ( [ 'filename', 'name' ] as $param_name )
    {
      // RFC 2231 z kontynuacją: param*0*=...; param*1*=...
      $rfc2231_parts = [];
      if ( preg_match_all( '/' . preg_quote( $param_name, '/' ) . '\*(\d+)\*?\s*=\s*([^\r\n;]+)/i', $headers, $matches, PREG_SET_ORDER ) )
      {
        foreach ( $matches as $m )
          $rfc2231_parts[(int)$m[1]] = trim( $m[2], " \t\"'" );

        if ( !empty( $rfc2231_parts ) )
        {
          ksort( $rfc2231_parts, SORT_NUMERIC );
          $combined = implode( '', $rfc2231_parts );
          if ( preg_match( "/^([^']*)'([^']*)'(.+)$/s", $combined, $enc ) )
          {
            $charset = strtoupper( $enc[1] );
            $decoded = rawurldecode( $enc[3] );
            if ( $charset !== '' && $charset !== 'UTF-8' && function_exists( 'mb_convert_encoding' ) )
              $decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset );
            $combined = $decoded;
          }
          if ( trim( $combined ) !== '' )
            $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $combined ) );
        }
      }

      // RFC 2231 prosty: param*=charset'lang'value
      if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\*\s*=\s*([^\r\n;]+)/i', $headers, $m ) )
      {
        $val = trim( $m[1], " \t\"'" );
        if ( preg_match( "/^([^']*)'([^']*)'(.+)$/s", $val, $enc ) )
        {
          $charset = strtoupper( $enc[1] );
          $decoded = rawurldecode( $enc[3] );
          if ( $charset !== '' && $charset !== 'UTF-8' && function_exists( 'mb_convert_encoding' ) )
            $decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset );
          $val = $decoded;
        }
        if ( trim( $val ) !== '' )
          $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
      }

      // Standardowy: param="value" lub param=value
      if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\s*=\s*"([^"]+)"/i', $headers, $m ) )
      {
        $val = $this -> decodeHeaderValue( trim( $m[1] ) );
        if ( trim( $val ) !== '' )
          $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
      }

      if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\s*=\s*([^\s;]+)/i', $headers, $m ) )
      {
        $val = $this -> decodeHeaderValue( trim( $m[1], " \t\"'" ) );
        if ( trim( $val ) !== '' )
          $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
      }

      // Wartość bez cudzysłowów, ale ze spacjami aż do końca linii lub średnika
      if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\s*=\s*([^;\r\n]+)/i', $headers, $m ) )
      {
        $val = $this -> decodeHeaderValue( trim( $m[1], " \t\"'" ) );
        if ( trim( $val ) !== '' )
          $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
      }
    }

    if ( preg_match( '/^\s*content-description\s*:\s*(.+)$/im', $headers, $m ) )
    {
      $val = $this -> decodeHeaderValue( trim( $m[1] ) );
      if ( trim( $val ) !== '' )
        $best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
    }

    return trim( $best_name );
  }

  public function debugExtractNameFromMimeHeaders( $headers )
  {
    return $this -> extractNameFromMimeHeaders( $headers );
  }

  private function extractPartContext( $part )
  {
    if ( !$part )
      return [];

    $params = $this -> partParams( $part );
    $filename_param = isset( $params['filename'] ) ? $this -> decodeHeaderValue( $params['filename'] ) : '';
    $name_param = isset( $params['name'] ) ? $this -> decodeHeaderValue( $params['name'] ) : '';
    $description_param = isset( $params['description'] ) ? $this -> decodeHeaderValue( $params['description'] ) : '';

    $name = $this -> pickBetterAttachmentName( $filename_param, $name_param );
    $name = $this -> pickBetterAttachmentName( $name, $description_param );
    if ( isset( $part -> description ) && trim( (string)$part -> description ) !== '' )
      $name = $this -> pickBetterAttachmentName( $name, $this -> decodeHeaderValue( (string)$part -> description ) );
    $disposition = isset( $part -> disposition ) ? strtoupper( trim( (string)$part -> disposition ) ) : '';

    $content_id = '';
    if ( isset( $part -> id ) and trim( (string)$part -> id ) !== '' )
      $content_id = $this -> normalizeContentId( (string)$part -> id );
    elseif ( isset( $params['content-id'] ) and trim( (string)$params['content-id'] ) !== '' )
      $content_id = $this -> normalizeContentId( (string)$params['content-id'] );

    return [
      'name' => $name,
      'disposition' => $disposition,
      'content_id' => $content_id
    ];
  }

  private function mergePartContexts( array $parent_context, array $child_context )
  {
    $merged = $parent_context;

    $parent_name = isset( $parent_context['name'] ) ? (string)$parent_context['name'] : '';
    $child_name = isset( $child_context['name'] ) ? (string)$child_context['name'] : '';
    $merged['name'] = $this -> pickBetterAttachmentName( $parent_name, $child_name );

    if ( isset( $child_context['disposition'] ) && trim( (string)$child_context['disposition'] ) !== '' )
      $merged['disposition'] = (string)$child_context['disposition'];
    elseif ( isset( $parent_context['disposition'] ) )
      $merged['disposition'] = (string)$parent_context['disposition'];

    if ( isset( $child_context['content_id'] ) && trim( (string)$child_context['content_id'] ) !== '' )
      $merged['content_id'] = (string)$child_context['content_id'];
    elseif ( isset( $parent_context['content_id'] ) )
      $merged['content_id'] = (string)$parent_context['content_id'];

    return $merged;
  }

  private function fetchRawMimeHeaders( $imap, $message_no, $part_number )
  {
    if ( $part_number === null )
      return '';

    $headers = (string)@imap_fetchmime( $imap, $message_no, $part_number, FT_PEEK );
    if ( trim( $headers ) === '' )
      $headers = (string)@imap_fetchbody( $imap, $message_no, $part_number . '.MIME', FT_PEEK );

    return (string)$headers;
  }

  private function decodePartBody( $raw, $encoding )
  {
    if ( $encoding === 3 )
      return (string)base64_decode( $raw );

    if ( $encoding === 4 )
      return (string)quoted_printable_decode( $raw );

    return (string)$raw;
  }

  private function mimeType( $part )
  {
    $primary = [
      0 => 'text',
      1 => 'multipart',
      2 => 'message',
      3 => 'application',
      4 => 'audio',
      5 => 'image',
      6 => 'video',
      7 => 'other'
    ];

    $type = isset( $part -> type ) ? (int)$part -> type : 0;
    $subtype = isset( $part -> subtype ) ? strtolower( (string)$part -> subtype ) : 'plain';

    return ( isset( $primary[$type] ) ? $primary[$type] : 'text' ) . '/' . $subtype;
  }

  private function htmlToText( $html )
  {
    $html = (string)$html;
    if ( trim( $html ) === '' )
      return '';

    $html = preg_replace( '/<style\b[^>]*>.*?<\/style>/is', ' ', $html );
    $html = preg_replace( '/<script\b[^>]*>.*?<\/script>/is', ' ', $html );
    $html = preg_replace( '/<blockquote\b[^>]*>.*?<\/blockquote>/is', ' ', $html );
    $html = preg_replace( '/<div[^>]*class="[^"]*(gmail_quote|gmail_signature)[^"]*"[^>]*>.*?<\/div>/is', ' ', $html );
    $html = preg_replace( '/<br\s*\/?>/i', "\n", $html );
    $html = preg_replace( '/<\/(p|div|li|tr|h[1-6])>/i', "\n", $html );
    $text = html_entity_decode( strip_tags( $html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );

    return $text;
  }

  private function cleanBodyText( $text )
  {
    $text = str_replace( [ "\r\n", "\r" ], "\n", (string)$text );
    $text = preg_replace( '/\x{00a0}/u', ' ', $text );

    $lines = explode( "\n", $text );
    $clean = [];

    foreach ( $lines as $line )
    {
      $line_trim = rtrim( $line );

      if ( preg_match( '/^\s*>+/', $line_trim ) )
        break;

      if ( preg_match( '/^\s*On\s.+wrote:\s*$/i', $line_trim ) )
        break;

      if ( preg_match( '/^\s*W\s+dniu\s+.+napisal\(a\):\s*$/iu', $line_trim ) )
        break;

      if ( preg_match( '/^\s*-----Original Message-----\s*$/i', $line_trim ) )
        break;

      if ( preg_match( '/^\s*Od:\s.+$/iu', $line_trim ) )
        break;

      if ( preg_match( '/^\s*From:\s.+$/i', $line_trim ) )
        break;

      if ( preg_match( '/^\s*Sent:\s.+$/i', $line_trim ) )
        break;

      if ( preg_match( '/^\s*--\s*$/', $line_trim ) )
        break;

      // Polskie stopki
      if ( preg_match( '/^\s*(Pozdrawiam|Pozdrowienia|Z\s+powa[zż]aniem|Dzi[eę]kuj[eę]|Serdecznie|Miłego\s+dnia)/iu', $line_trim ) )
        break;

      // Angielskie stopki
      if ( preg_match( '/^\s*(Best\s+regards|Kind\s+regards|Regards|Sincerely|Thanks|Thank\s+you|Cheers)/i', $line_trim ) )
        break;

      // Mobilne sygnatury
      if ( preg_match( '/^\s*Sent\s+from\s+(my\s+)?(iPhone|iPad|Android|Samsung|mobile)/i', $line_trim ) )
        break;

      // Separatory z podkreślników
      if ( preg_match( '/^\s*_{3,}\s*$/', $line_trim ) )
        break;

      $clean[] = $line_trim;
    }

    $text = trim( implode( "\n", $clean ) );

    return trim( $text );
  }

  private function prepareImportedTaskText( $text )
  {
    $text = trim( (string)$text );
    if ( $text === '' )
      return '(brak tresci)';

    if ( preg_match( '/<\s*[a-z][^>]*>/i', $text ) )
    {
      $text = $this -> htmlToText( $text );
      $text = $this -> cleanBodyText( $text );
    }

    $text = str_replace( [ "\r\n", "\r" ], "\n", $text );
    $text = trim( $text );

    if ( $text === '' )
      return '(brak tresci)';

    return nl2br( $text );
  }

  private function shouldImportAttachment( array $part, array $cid_refs )
  {
    $name = trim( isset( $part['name'] ) ? (string)$part['name'] : '' );
    $mime = strtolower( isset( $part['mime'] ) ? (string)$part['mime'] : '' );
    $disposition = strtoupper( isset( $part['disposition'] ) ? (string)$part['disposition'] : '' );
    $content_id = $this -> normalizeContentId( isset( $part['content_id'] ) ? (string)$part['content_id'] : '' );
    $body = isset( $part['body'] ) ? (string)$part['body'] : '';

    if ( !empty( $part['is_inline'] ) )
      return false;

    if ( $mime === 'text/plain' or $mime === 'text/html' )
      return false;

    if ( $content_id !== '' and isset( $cid_refs[$content_id] ) )
      return false;

    if ( strpos( $mime, 'image/' ) === 0 and $disposition !== 'ATTACHMENT' )
      return false;

    if ( $name !== '' )
      return true;

    if ( $disposition === 'ATTACHMENT' )
      return true;

    if ( $mime === 'application/pdf' or $mime === 'application/x-pdf' )
      return true;

    if ( $mime === 'application/octet-stream' and self::contentLooksLikePdf( $body ) )
      return true;

    return false;
  }

  private function isLikelyBinaryAttachmentWithoutName( $mime, $content, $disposition, $content_id )
  {
    $mime = strtolower( trim( (string)$mime ) );
    $disposition = strtoupper( trim( (string)$disposition ) );
    $content_id = trim( (string)$content_id );

    if ( strpos( $mime, 'text/' ) === 0 )
      return false;

    if ( $content_id !== '' )
      return false;

    if ( strpos( $mime, 'image/' ) === 0 && $disposition !== 'ATTACHMENT' )
      return false;

    if ( $mime === 'application/pdf' || $mime === 'application/x-pdf' )
      return true;

    if ( $mime === 'application/octet-stream' && self::contentLooksLikePdf( $content ) )
      return true;

    return false;
  }

  private static function contentLooksLikePdf( $content )
  {
    if ( !is_string( $content ) || $content === '' )
      return false;

    return strpos( substr( $content, 0, 1024 ), '%PDF-' ) !== false;
  }

  private function extractReferencedCidValues( $html )
  {
    $html = (string)$html;
    if ( trim( $html ) === '' )
      return [];

    preg_match_all( '/cid:([^"\'>\s]+)/i', $html, $matches );
    if ( !isset( $matches[1] ) or !is_array( $matches[1] ) or !count( $matches[1] ) )
      return [];

    $refs = [];
    foreach ( $matches[1] as $raw )
    {
      $cid = $this -> normalizeContentId( $raw );
      if ( $cid !== '' )
        $refs[$cid] = true;
    }

    return $refs;
  }

  private function normalizeContentId( $value )
  {
    $value = trim( (string)$value );
    if ( $value === '' )
      return '';

    $value = trim( $value, '<>' );
    return strtolower( $value );
  }

  private function parseWithAI( $api_key, $model, $subject, $raw_content, $attempt = 1 )
  {
    $this -> last_ai_error = '';
    $api_key = trim( (string)$api_key );
    $subject = trim( (string)$subject );
    $raw_content = trim( (string)$raw_content );
    $model = trim( (string)$model );

    if ( $api_key === '' )
    {
      $this -> last_ai_error = 'Brak klucza API OpenAI.';
      return null;
    }

    if ( $model === '' )
      $model = 'gpt-4o-mini';

    if ( $raw_content === '' )
    {
      $this -> last_ai_error = 'Pusta tresc emaila do analizy AI.';
      return null;
    }

    if ( !function_exists( 'curl_init' ) )
    {
      $this -> last_ai_error = 'Brak rozszerzenia cURL.';
      return null;
    }

    // Przygotuj treść do analizy (usuń HTML jesli jest)
    if ( preg_match( '/<\s*[a-z][^>]*>/i', $raw_content ) )
    {
      $raw_content = $this -> htmlToText( $raw_content );
      $raw_content = $this -> cleanBodyText( $raw_content );
    }

    $content_limit = $attempt > 1 ? 1200 : 1800;
    $prompt = "Jesteś asystentem do przetwarzania emaili na zadania w systemie CRM. " .
              "Na podstawie poniższego tematu i treści emaila wygeneruj zadanie wdrożeniowe, NIE podsumowanie. " .
              "Opis ma być szczegółową listą rzeczy do wykonania dla zespołu.\n\n" .
              "Wymagania:\n" .
              "- Nie pomijaj żadnych wymagań, liczb, zakresów cen, CTA, pól formularza, kroków i terminów.\n" .
              "- Zamień treść emaila na checklistę implementacyjną.\n" .
              "- Zachowaj strukturę sekcji i podsekcji.\n" .
              "- Pisz konkretnie co zmienić, gdzie zmienić i jakie treści dodać.\n" .
              "- Nie używaj ogólników typu: \"zaktualizować stronę\".\n\n" .
              "Format pola task_text:\n" .
              "1) Cel zmiany\n" .
              "2) Zakres zmian (lista punktów)\n" .
              "3) Treści do wstawienia (dokładne teksty/liczby)\n" .
              "4) Zmiany formularza\n" .
              "5) Wersja sezonowa / warunki wyświetlania\n" .
              "6) CTA i bonusy\n" .
              "7) Kryteria akceptacji\n\n" .
              "Temat emaila: " . $subject . "\n\n" .
              "Treść emaila:\n" . mb_substr( $raw_content, 0, $content_limit ) . "\n\n" .
              "Zwróć TYLKO poprawny JSON:\n" .
              '{"task_name": "krótki temat zadania", "task_text": "szczegółowa checklista wdrożeniowa"}';

    $payload = [
      'model' => $model,
      'messages' => [
        [
          'role' => 'system',
          'content' => 'Jesteś asystentem do przetwarzania emaili. Odpowiadasz TYLKO w formacie JSON i tworzysz szczegółowe checklisty wdrożeniowe, nie skróty.'
        ],
        [
          'role' => 'user',
          'content' => $prompt
        ]
      ]
    ];

    if ( stripos( $model, 'gpt-5' ) === 0 )
      $payload['response_format'] = [ 'type' => 'json_object' ];

    if ( stripos( $model, 'gpt-5' ) === 0 )
      $payload['max_completion_tokens'] = $attempt > 1 ? 1600 : 1000;
    else
    {
      $payload['temperature'] = 0.3;
      $payload['max_tokens'] = 1200;
    }

    $ch = curl_init( 'https://api.openai.com/v1/chat/completions' );
    curl_setopt_array( $ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_POSTFIELDS => json_encode( $payload ),
      CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $api_key
      ],
      CURLOPT_TIMEOUT => 30
    ] );

    $response = curl_exec( $ch );
    $curl_error = curl_error( $ch );
    $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
    curl_close( $ch );

    if ( $response === false )
    {
      $this -> last_ai_error = 'Blad cURL: ' . $curl_error;
      return null;
    }

    if ( $http_code !== 200 )
    {
      $this -> last_ai_error = 'HTTP ' . $http_code . ' z OpenAI: ' . $this -> extractApiErrorMessage( $response );
      return null;
    }

    if ( !$response )
    {
      $this -> last_ai_error = 'Pusta odpowiedz z OpenAI.';
      return null;
    }

    $data = json_decode( $response, true );
    if ( !is_array( $data ) )
    {
      $this -> last_ai_error = 'Niepoprawny JSON z OpenAI: ' . json_last_error_msg();
      return null;
    }

    if ( isset( $data['error']['message'] ) )
    {
      $this -> last_ai_error = 'OpenAI error: ' . (string)$data['error']['message'];
      return null;
    }

    if ( !isset( $data['choices'][0] ) || !is_array( $data['choices'][0] ) )
    {
      $this -> last_ai_error = 'Brak choices[0] w odpowiedzi OpenAI. RAW: ' . $this -> clipForLog( $response );
      return null;
    }

    $choice = $data['choices'][0];
    $content = $this -> extractModelMessageContent( $choice );
    if ( $content === '' )
    {
      $finish_reason = isset( $choice['finish_reason'] ) ? (string)$choice['finish_reason'] : '(brak)';
      $refusal = isset( $choice['message']['refusal'] ) ? (string)$choice['message']['refusal'] : '';

      if ( $finish_reason === 'length' && (int)$attempt === 1 )
      {
        $retry_content = mb_substr( $raw_content, 0, 1200 );
        return $this -> parseWithAI( $api_key, $model, $subject, $retry_content, 2 );
      }

      $error = 'Pusta tresc odpowiedzi modelu. finish_reason=' . $finish_reason;
      if ( $refusal !== '' )
        $error .= ', refusal=' . $this -> clipForLog( $refusal );

      $this -> last_ai_error = $error . '. RAW: ' . $this -> clipForLog( $response );
      return null;
    }

    // Usuń markdown code block jeśli jest
    $content = preg_replace( '/```json\s*|\s*```/', '', $content );
    $content = trim( $content );

    // Jeśli model zwrócił JSON z komentarzem wokół, wyciągnij tylko obiekt.
    $json_start = strpos( $content, '{' );
    $json_end = strrpos( $content, '}' );
    if ( $json_start !== false && $json_end !== false && $json_end > $json_start )
      $content = substr( $content, $json_start, $json_end - $json_start + 1 );

    $parsed = json_decode( $content, true );
    if ( !is_array( $parsed ) || !isset( $parsed['task_name'] ) || !isset( $parsed['task_text'] ) )
    {
      $this -> last_ai_error = 'Niepoprawny JSON z modelu. Odpowiedz: ' . $this -> clipForLog( $content );
      return null;
    }

    $task_name = $this -> normalizeAiTextValue( $parsed['task_name'] );
    $task_text = $this -> normalizeAiTextValue( $parsed['task_text'] );

    if ( $task_name === '' )
      $task_name = '(bez tematu)';

    if ( $task_text === '' )
      $task_text = '(brak treści)';

    // Formatuj treść zadania z nl2br
    $task_text = nl2br( $task_text );

    return [
      'task_name' => $task_name,
      'task_text' => $task_text
    ];
  }

  private function extractApiErrorMessage( $response )
  {
    $data = json_decode( (string)$response, true );
    if ( is_array( $data ) && isset( $data['error']['message'] ) )
      return (string)$data['error']['message'];

    return $this -> clipForLog( $response );
  }

  private function extractModelMessageContent( array $choice )
  {
    if ( isset( $choice['message']['content'] ) && is_string( $choice['message']['content'] ) )
      return trim( $choice['message']['content'] );

    if ( isset( $choice['message']['content'] ) && is_array( $choice['message']['content'] ) )
    {
      $chunks = [];
      foreach ( $choice['message']['content'] as $part )
      {
        if ( is_string( $part ) )
          $chunks[] = $part;
        elseif ( is_array( $part ) )
        {
          if ( isset( $part['text'] ) && is_string( $part['text'] ) )
            $chunks[] = $part['text'];
          elseif ( isset( $part['text']['value'] ) && is_string( $part['text']['value'] ) )
            $chunks[] = $part['text']['value'];
        }
      }

      return trim( implode( "\n", $chunks ) );
    }

    if ( isset( $choice['message']['tool_calls'][0]['function']['arguments'] ) && is_string( $choice['message']['tool_calls'][0]['function']['arguments'] ) )
      return trim( $choice['message']['tool_calls'][0]['function']['arguments'] );

    if ( isset( $choice['message']['function_call']['arguments'] ) && is_string( $choice['message']['function_call']['arguments'] ) )
      return trim( $choice['message']['function_call']['arguments'] );

    return '';
  }

  private function clipForLog( $value, $limit = 600 )
  {
    $text = trim( (string)$value );
    if ( $text === '' )
      return '';

    return mb_substr( $text, 0, (int)$limit );
  }

  private function normalizeAiTextValue( $value )
  {
    if ( is_string( $value ) )
      return trim( $value );

    if ( is_numeric( $value ) || is_bool( $value ) )
      return trim( (string)$value );

    if ( is_array( $value ) )
    {
      $lines = [];
      foreach ( $value as $item )
      {
        $item_text = $this -> normalizeAiTextValue( $item );
        if ( $item_text !== '' )
          $lines[] = $item_text;
      }

      return trim( implode( "\n", $lines ) );
    }

    if ( is_object( $value ) )
    {
      $array_value = json_decode( json_encode( $value ), true );
      if ( is_array( $array_value ) )
      {
        $normalized = $this -> normalizeAiTextValue( $array_value );
        if ( $normalized !== '' )
          return $normalized;
      }

      return trim( json_encode( $value ) );
    }

    return '';
  }

  private function guessExtensionFromMime( $mime )
  {
    $ext = $this -> extensionFromMime( $mime );
    return $ext !== '' ? $ext : 'png';
  }

  private function extensionFromMime( $mime )
  {
    $map = [
      'application/pdf' => 'pdf',
      'application/zip' => 'zip',
      'application/x-rar-compressed' => 'rar',
      'application/vnd.rar' => 'rar',
      'application/x-7z-compressed' => '7z',
      'application/msword' => 'doc',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
      'application/vnd.ms-excel' => 'xls',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
      'application/vnd.ms-powerpoint' => 'ppt',
      'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
      'application/xml' => 'xml',
      'application/json' => 'json',
      'application/rtf' => 'rtf',
      'application/octet-stream' => '',
      'text/plain' => 'txt',
      'text/html' => 'html',
      'text/csv' => 'csv',
      'image/jpeg' => 'jpg',
      'image/png' => 'png',
      'image/gif' => 'gif',
      'image/webp' => 'webp',
      'image/bmp' => 'bmp',
      'image/svg+xml' => 'svg',
      'image/tiff' => 'tiff'
    ];

    $mime = strtolower( trim( (string)$mime ) );
    return isset( $map[$mime] ) ? $map[$mime] : '';
  }

  private function pickBetterAttachmentName( $primary, $secondary )
  {
    $primary = trim( (string)$primary );
    $secondary = trim( (string)$secondary );

    if ( $primary === '' )
      return $secondary;

    if ( $secondary === '' )
      return $primary;

    $primary_score = self::attachmentNameScore( $primary );
    $secondary_score = self::attachmentNameScore( $secondary );

    return $secondary_score > $primary_score ? $secondary : $primary;
  }

  public static function normalizeImportedAttachmentName( $name, $mime = '', $content = null )
  {
    $name = trim( (string)$name );
    $technical_ext = '';

    if ( self::isTechnicalAttachmentName( $name ) )
    {
      $technical_ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
      $name = '';
    }

    if ( $name === '' )
      $name = 'zalacznik';

    $ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
    if ( $ext === '' )
    {
      $mime_ext = self::extensionFromMimeStatic( $mime );
      if ( $mime_ext === '' && $technical_ext !== '' )
        $mime_ext = $technical_ext;
      if ( $mime_ext === '' )
        $mime_ext = self::extensionFromContent( $content );
      if ( $mime_ext !== '' )
        $name .= '.' . $mime_ext;
    }

    return $name;
  }

  private static function attachmentNameScore( $name )
  {
    $name = trim( (string)$name );
    if ( $name === '' )
      return -1;

    $score = 1;
    if ( pathinfo( $name, PATHINFO_EXTENSION ) !== '' )
      $score += 2;

    if ( self::isTechnicalAttachmentName( $name ) )
      $score -= 3;

    return $score;
  }

  private static function isTechnicalAttachmentName( $name )
  {
    $name = trim( (string)$name );
    if ( $name === '' )
      return false;

    return (bool)preg_match( '/^att_[a-f0-9]{8,}(?:\.[a-z0-9]{2,6})?$/i', $name );
  }

  private static function extensionFromMimeStatic( $mime )
  {
    $map = [
      'application/pdf' => 'pdf',
      'application/zip' => 'zip',
      'application/x-rar-compressed' => 'rar',
      'application/vnd.rar' => 'rar',
      'application/x-7z-compressed' => '7z',
      'application/msword' => 'doc',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
      'application/vnd.ms-excel' => 'xls',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
      'application/vnd.ms-powerpoint' => 'ppt',
      'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
      'application/xml' => 'xml',
      'application/json' => 'json',
      'application/rtf' => 'rtf',
      'application/octet-stream' => '',
      'text/plain' => 'txt',
      'text/html' => 'html',
      'text/csv' => 'csv',
      'image/jpeg' => 'jpg',
      'image/png' => 'png',
      'image/gif' => 'gif',
      'image/webp' => 'webp',
      'image/bmp' => 'bmp',
      'image/svg+xml' => 'svg',
      'image/tiff' => 'tiff'
    ];

    $mime = strtolower( trim( (string)$mime ) );
    return isset( $map[$mime] ) ? $map[$mime] : '';
  }

  private static function extensionFromContent( $content )
  {
    if ( !is_string( $content ) || $content === '' )
      return '';

    $prefix = substr( $content, 0, 16 );

    if ( substr( $prefix, 0, 4 ) === '%PDF' )
      return 'pdf';

    if ( substr( $prefix, 0, 2 ) === 'PK' )
      return 'zip';

    if ( substr( $prefix, 0, 3 ) === "\xFF\xD8\xFF" )
      return 'jpg';

    if ( substr( $prefix, 0, 8 ) === "\x89PNG\r\n\x1A\n" )
      return 'png';

    if ( substr( $prefix, 0, 6 ) === 'GIF87a' || substr( $prefix, 0, 6 ) === 'GIF89a' )
      return 'gif';

    return '';
  }

  private function replaceCidReferences( $html, array $cid_to_url )
  {
    if ( trim( (string)$html ) === '' || empty( $cid_to_url ) )
      return $html;

    foreach ( $cid_to_url as $cid => $url )
    {
      // Zastąp wszystkie odniesienia do tego CID
      $html = preg_replace(
        '/(<img[^>]+src=["\'])cid:' . preg_quote( $cid, '/' ) . '(["\'][^>]*>)/i',
        '$1' . $url . '$2',
        $html
      );
    }

    return $html;
  }

  private function prepareImportedTaskTextFromHtml( $html )
  {
    $html = trim( (string)$html );
    if ( $html === '' )
      return '(brak tresci)';

    // Usuń niepotrzebne elementy (style, script, blockquote)
    $html = preg_replace( '/<style\b[^>]*>.*?<\/style>/is', ' ', $html );
    $html = preg_replace( '/<script\b[^>]*>.*?<\/script>/is', ' ', $html );
    $html = preg_replace( '/<blockquote\b[^>]*>.*?<\/blockquote>/is', ' ', $html );
    $html = preg_replace( '/<div[^>]*class="[^"]*(gmail_quote|gmail_signature)[^"]*"[^>]*>.*?<\/div>/is', ' ', $html );

    // Usuń tagi MS Office: <o:p>, </o:p>, <w:sdt>, itp.
    $html = preg_replace( '/<\/?[a-z]+:[^>]*>/is', '', $html );

    // Usuń xmlns atrybuty
    $html = preg_replace( '/\s+xmlns:[a-z]+="[^"]*"/i', '', $html );

    // Usuń atrybuty class (MsoNormal, itp.)
    $html = preg_replace( '/\s+class="[^"]*"/i', '', $html );

    // Usuń atrybuty style
    $html = preg_replace( '/\s+style="[^"]*"/i', '', $html );

    // Usuń puste paragrafy (z samym &nbsp; lub spacjami)
    $html = preg_replace( '/<p[^>]*>\s*(&nbsp;|\xC2\xA0)?\s*<\/p>/i', '', $html );

    // Usuń niepotrzebne atrybuty z obrazów (zostaw tylko src, alt, width, height)
    $html = preg_replace_callback(
      '/<img([^>]*)>/i',
      function( $matches ) {
        $attrs = $matches[1];
        $new_attrs = [];

        if ( preg_match( '/\bsrc=["\']([^"\']+)["\']/i', $attrs, $src ) )
          $new_attrs[] = 'src="' . htmlspecialchars( $src[1], ENT_QUOTES ) . '"';

        if ( preg_match( '/\balt=["\']([^"\']+)["\']/i', $attrs, $alt ) )
          $new_attrs[] = 'alt="' . htmlspecialchars( $alt[1], ENT_QUOTES ) . '"';

        if ( preg_match( '/\bwidth=["\']?(\d+)["\']?/i', $attrs, $width ) )
          $new_attrs[] = 'width="' . (int)$width[1] . '"';

        if ( preg_match( '/\bheight=["\']?(\d+)["\']?/i', $attrs, $height ) )
          $new_attrs[] = 'height="' . (int)$height[1] . '"';

        return '<img ' . implode( ' ', $new_attrs ) . '>';
      },
      $html
    );

    // Wyczyść HTML z nadmiarowych białych znaków
    $html = preg_replace( '/\s+/', ' ', $html );
    $html = trim( $html );

    if ( $html === '' )
      return '(brak tresci)';

    return $html;
  }
}
