<?php
namespace Domain\Finances;

class FakturowniaInvoiceImporter
{
  private $mdb;
  private $repo;
  private $apiClient;
  private $startDate;
  private $pageLimit = 100;
  private $ownTaxNo = '';

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

    $this -> repo = new FakturowniaImportRepository( $this -> mdb );
  }

  public function import()
  {
    $this -> repo -> ensureTables();

    $config = $this -> resolveConfig();
    if ( !$config['ok'] )
      return $config;

    $this -> startDate = $config['start_date'];
    $this -> apiClient = new FakturowniaApiClient(
      $config['api_url'],
      $config['token'],
      $config['page_limit']
    );
    $this -> pageLimit = (int)$config['page_limit'];
    $this -> ownTaxNo = (string)$config['own_tax_no'];

    $summary = [
      'imported' => 0,
      'skipped' => 0,
      'unmapped' => 0,
      'errors' => 0
    ];

    $summary = $this -> processDocumentType( 'income', $summary );
    $summary = $this -> processDocumentType( 'cost', $summary );

    $lastSummary = json_encode( [
      'at' => date( 'Y-m-d H:i:s' ),
      'summary' => $summary
    ], JSON_UNESCAPED_UNICODE );
    $this -> repo -> saveState( 'last_import_summary', $lastSummary );

    if ( $summary['errors'] > 0 )
      return [ 'status' => 'error', 'msg' => $this -> formatMessage( $summary ), 'summary' => $summary ];

    if ( $summary['imported'] === 0 )
      return [ 'status' => 'empty', 'msg' => $this -> formatMessage( $summary ), 'summary' => $summary ];

    return [ 'status' => 'ok', 'msg' => $this -> formatMessage( $summary ), 'summary' => $summary ];
  }

  private function processDocumentType( $documentType, $summary )
  {
    $page = 1;

    while ( true )
    {
      try
      {
        if ( $documentType === 'income' )
          $documents = $this -> apiClient -> fetchSalesDocuments( $this -> startDate, $page );
        else
          $documents = $this -> apiClient -> fetchCostDocuments( $this -> startDate, $page );
      }
      catch ( \Throwable $e )
      {
        $summary['errors']++;
        break;
      }

      if ( !is_array( $documents ) || empty( $documents ) )
        break;

      foreach ( $documents as $document )
      {
        $result = $this -> processSingleDocument( $document, $documentType );
        $summary[ $result ]++;
      }

      if ( count( $documents ) < $this -> pageLimit )
        break;

      // Nie przerywamy petli na podstawie dat w liscie — API moze sortowac po updated_at/created_at
      // zamiast issue_date, co powodowalo gubienie faktur z wcześniejsza data wystawienia.
      // Filtr "za stare" dziala per-dokument w processSingleDocument (strtotime(date) < startDate).
      $page++;
      if ( $page > 100 )
        break;
    }

    return $summary;
  }

  private function processSingleDocument( $rawDocument, $documentType )
  {
    if ( !$this -> matchesDocumentType( $rawDocument, $documentType ) )
      return 'skipped';

    if ( $this -> isProformaDocument( $rawDocument ) )
      return 'skipped';

    $document = $this -> normalizeDocument( $rawDocument, $documentType );
    if ( !$document )
      return 'skipped';

    if ( $this -> repo -> isDocumentImported( $document['document_key'] ) )
      return 'skipped';

    if ( strtotime( $document['date'] ) < strtotime( $this -> startDate ) )
      return 'skipped';

    $clientMap = $this -> repo -> getClientMapping( $document['client_key'] );
    if ( !$clientMap && $document['legacy_client_key'] !== '' && $document['legacy_client_key'] !== $document['client_key'] )
    {
      // Backward compatibility for historical mappings saved as id:{client_id}.
      $clientMap = $this -> repo -> getClientMapping( $document['legacy_client_key'] );
    }

    if ( !$clientMap )
    {
      $positionNames = [];
      foreach ( $document['positions'] as $pos )
      {
        $name = trim( (string)( $pos['name'] ?? '' ) );
        if ( $name !== '' )
          $positionNames[] = $name;
      }

      $this -> repo -> queueUnmapped( 'client', $document['client_key'], $document['client_name'], [
        'document_id' => $document['external_id'],
        'document_number' => $document['number'],
        'document_type' => $documentType,
        'tax_no' => $document['client_tax_no'],
        'positions' => $positionNames
      ] );
      return 'unmapped';
    }

    $resolvedPositions = [];
    $skippedPositions = [];
    foreach ( $document['positions'] as $position )
    {
      // Per-dokument skip: uzytkownik oznaczyl konkretna pozycje na konkretnej fakturze jako pomijana.
      // Pomijanie dzieje sie ZANIM sprawdzamy mapowanie, zeby pominieta pozycja nie blokowala importu.
      if ( $this -> repo -> isDocumentPositionSkipped( $document['external_id'], $documentType, $position['item_key'] ) )
      {
        $skippedPositions[] = [
          'item_key' => $position['item_key'],
          'name' => $position['name']
        ];
        continue;
      }

      $itemMap = $this -> repo -> getItemMapping( $position['item_key'] );
      if ( !$itemMap )
      {
        $this -> repo -> queueUnmapped( 'item', $position['item_key'], $position['name'], [
          'document_id' => $document['external_id'],
          'document_number' => $document['number'],
          'document_type' => $documentType,
          'buyer_name' => $document['buyer_name'],
          'seller_name' => $document['seller_name'],
          'position_name' => $position['name']
        ] );
        return 'unmapped';
      }

      $amount = $this -> normalizeAmount( $position['amount'], $documentType );
      if ( $amount == 0.0 )
        continue;

      $resolvedPositions[] = [
        'category_id' => (int)$itemMap['finance_category_id'],
        'amount' => $amount,
        'description' => $this -> buildDescription( $document, $position )
      ];
    }

    // Dokument bez zadnych pozycji do przetworzenia (ani resolved, ani skip) → nic nie zapisujemy.
    // Nie oznaczamy jako imported, zeby nie zablokowal sie na zawsze.
    if ( empty( $resolvedPositions ) && empty( $skippedPositions ) )
      return 'skipped';

    $operationIds = [];

    // Cala faktura importuje sie atomowo: albo wszystkie pozycje, albo nic.
    $this -> mdb -> pdo -> beginTransaction();
    try
    {
      foreach ( $resolvedPositions as $resolvedPosition )
      {
        $this -> mdb -> insert( 'finance_operations', [
          'date' => $document['date'],
          'category_id' => $resolvedPosition['category_id'],
          'amount' => $resolvedPosition['amount'],
          'description' => $resolvedPosition['description'],
          'client_id' => (int)$clientMap['crm_client_id']
        ] );

        $operationIds[] = (int)$this -> mdb -> id();
      }

      $this -> repo -> markDocumentImported(
        $document['document_key'],
        $documentType,
        $document['external_id'],
        $document['date'],
        $document['total_amount'],
        $operationIds,
        [
          'number' => $document['number'],
          'client_name' => $document['client_name'],
          'skipped_positions' => $skippedPositions
        ]
      );

      $this -> mdb -> pdo -> commit();
    }
    catch ( \Throwable $e )
    {
      if ( $this -> mdb -> pdo ->inTransaction() )
        $this -> mdb -> pdo -> rollBack();

      throw $e;
    }

    return 'imported';
  }

  private function matchesDocumentType( $rawDocument, $documentType )
  {
    if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) )
      $rawDocument = $rawDocument['invoice'];

    if ( !is_array( $rawDocument ) )
      return false;

    if ( !array_key_exists( 'income', $rawDocument ) )
      return true;

    $incomeFlag = $rawDocument['income'];
    $isIncome = !in_array( $incomeFlag, [ false, 0, '0', 'false', 'FALSE' ], true );

    if ( $documentType === 'income' )
      return $isIncome;

    return !$isIncome;
  }

  private function isProformaDocument( $rawDocument )
  {
    if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) )
      $rawDocument = $rawDocument['invoice'];

    if ( !is_array( $rawDocument ) )
      return false;

    if ( isset( $rawDocument['kind'] ) && strtolower( (string)$rawDocument['kind'] ) === 'proforma' )
      return true;

    $number = (string)( $rawDocument['number'] ?? $rawDocument['full_number'] ?? '' );
    if ( $number !== '' && strncasecmp( $number, 'FP', 2 ) === 0 )
      return true;

    return false;
  }

  private function normalizeDocument( $rawDocument, $documentType )
  {
    if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) )
      $rawDocument = $rawDocument['invoice'];

    if ( !is_array( $rawDocument ) )
      return null;

    $externalId = isset( $rawDocument['id'] ) ? (string)$rawDocument['id'] : '';
    if ( $externalId === '' )
      return null;

    $number = (string)( $rawDocument['number'] ?? $rawDocument['full_number'] ?? '' );
    $date = (string)( $rawDocument['issue_date'] ?? $rawDocument['sell_date'] ?? $rawDocument['created_at'] ?? date( 'Y-m-d' ) );
    $date = substr( $date, 0, 10 );

    if ( $documentType === 'income' )
    {
      $clientName = (string)( $rawDocument['buyer_name'] ?? $rawDocument['client_name'] ?? 'Nieznany klient' );
      $clientTaxNo = (string)( $rawDocument['buyer_tax_no'] ?? $rawDocument['tax_no'] ?? '' );
    }
    else
    {
      // Dla kosztow kontrahentem jest "druga strona" — ta, ktorej NIP != naszego.
      // Fakturownia dla wydatkow pobranych z KSeF (approval_status='received') odwraca role:
      // seller_* to dane wlasnej firmy, buyer_* to prawdziwy dostawca. Bez tej heurystyki
      // koszty z KSeF trafialyby do kolejki unmapped z wlasnym NIP-em jako klucz klienta.
      $sellerTaxNo = (string)( $rawDocument['seller_tax_no'] ?? '' );

      if ( $this -> taxNoEqualsOwn( $sellerTaxNo ) )
      {
        $clientName = (string)( $rawDocument['buyer_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' );
        $clientTaxNo = (string)( $rawDocument['buyer_tax_no'] ?? $rawDocument['tax_no'] ?? '' );
      }
      else
      {
        $clientName = (string)( $rawDocument['seller_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' );
        $clientTaxNo = $sellerTaxNo !== '' ? $sellerTaxNo : (string)( $rawDocument['tax_no'] ?? '' );
      }
    }

    $positions = [];
    $totalAmount = 0.0;

    $rawPositions = $this -> resolvePositions( $rawDocument );
    foreach ( $rawPositions as $rawPosition )
    {
      if ( !is_array( $rawPosition ) )
        continue;

      $name = trim( (string)( $rawPosition['name'] ?? '' ) );
      if ( $name === '' )
        $name = 'Pozycja';

      $amount = $this -> resolvePositionAmount( $rawPosition );
      $totalAmount += $amount;

      $positions[] = [
        'item_key' => $this -> buildItemKey( $rawPosition, $name ),
        'name' => $name,
        'amount' => $amount
      ];
    }

    if ( empty( $positions ) )
    {
      $fallback = (float)( $rawDocument['price_gross'] ?? $rawDocument['price'] ?? 0 );
      $fallbackName = trim( (string)( $rawDocument['product_cache'] ?? '' ) );
      if ( $fallbackName === '' )
        $fallbackName = 'Faktura bez pozycji';

      if ( $fallback != 0.0 )
      {
        $positions[] = [
          'item_key' => $this -> buildItemKey( [], $fallbackName ),
          'name' => $fallbackName,
          'amount' => $fallback
        ];
        $totalAmount = $fallback;
      }
    }

    if ( empty( $positions ) )
      return null;

    return [
      'external_id' => $externalId,
      'document_key' => $documentType . ':' . $externalId,
      'number' => $number,
      'date' => $date,
      'buyer_name' => (string)( $rawDocument['buyer_name'] ?? '' ),
      'seller_name' => (string)( $rawDocument['seller_name'] ?? '' ),
      'client_name' => $clientName,
      'client_tax_no' => $clientTaxNo,
      'client_key' => $this -> buildClientKey( $rawDocument, $clientName, $clientTaxNo ),
      'legacy_client_key' => $this -> buildLegacyClientKey( $rawDocument ),
      'positions' => $positions,
      'total_amount' => $this -> normalizeAmount( $totalAmount, $documentType )
    ];
  }

  private function resolvePositions( $rawDocument )
  {
    if ( isset( $rawDocument['positions'] ) && is_array( $rawDocument['positions'] ) && !empty( $rawDocument['positions'] ) )
      return $rawDocument['positions'];

    $invoiceId = isset( $rawDocument['id'] ) ? (int)$rawDocument['id'] : 0;
    if ( $invoiceId <= 0 )
      return [];

    $details = $this -> apiClient -> fetchInvoiceDetails( $invoiceId );
    if ( is_array( $details ) && isset( $details['positions'] ) && is_array( $details['positions'] ) )
      return $details['positions'];

    return [];
  }

  private function resolvePositionAmount( $position )
  {
    $quantity = (float)( $position['quantity'] ?? 1 );
    if ( $quantity <= 0 )
      $quantity = 1;

    $totalNet = (float)( $position['total_price_net'] ?? 0 );
    if ( $totalNet != 0.0 )
      return $totalNet;

    $unitNet = (float)( $position['price'] ?? $position['price_net'] ?? 0 );
    if ( $unitNet != 0.0 )
      return $unitNet * $quantity;

    // Fallback dla pozycji, ktore nie maja netto w payloadzie.
    $totalGross = (float)( $position['total_price_gross'] ?? 0 );
    if ( $totalGross != 0.0 )
      return $totalGross;

    $unitGross = (float)( $position['price_gross'] ?? 0 );
    if ( $unitGross != 0.0 )
      return $unitGross * $quantity;

    return 0.0;
  }

  private function buildClientKey( $rawDocument, $clientName, $clientTaxNo )
  {
    $normalizedTaxNo = $this -> normalizeTaxNo( $clientTaxNo );
    if ( $normalizedTaxNo !== '' )
      return 'tax:' . $normalizedTaxNo;

    $legacyClientKey = $this -> buildLegacyClientKey( $rawDocument );
    if ( $legacyClientKey !== '' )
      return $legacyClientKey;

    return 'name:' . $this -> normalizeKeyValue( $clientName );
  }

  private function buildLegacyClientKey( $rawDocument )
  {
    if ( isset( $rawDocument['client_id'] ) && (string)$rawDocument['client_id'] !== '' )
      return 'id:' . (string)$rawDocument['client_id'];

    return '';
  }

  private function normalizeTaxNo( $taxNo )
  {
    $normalizedTaxNo = preg_replace( '/\D+/', '', (string)$taxNo );
    if ( !is_string( $normalizedTaxNo ) || $normalizedTaxNo === '' )
      return '';

    return $normalizedTaxNo;
  }

  private function taxNoEqualsOwn( $taxNo )
  {
    if ( $this -> ownTaxNo === '' )
      return false;

    $normalized = $this -> normalizeTaxNo( $taxNo );
    if ( $normalized === '' )
      return false;

    return $normalized === $this -> normalizeTaxNo( $this -> ownTaxNo );
  }

  private function buildItemKey( $rawPosition, $name )
  {
    if ( isset( $rawPosition['product_id'] ) && (string)$rawPosition['product_id'] !== '' )
      return 'product:' . (string)$rawPosition['product_id'];

    return 'name:' . $this -> normalizeKeyValue( $name );
  }

  private function normalizeKeyValue( $value )
  {
    $value = trim( (string)$value );
    if ( function_exists( 'mb_strtolower' ) )
      return mb_strtolower( $value, 'UTF-8' );

    return strtolower( $value );
  }

  private function normalizeAmount( $amount, $documentType )
  {
    $amount = (float)$amount;
    if ( $documentType === 'cost' )
      return abs( $amount ) * -1;

    return abs( $amount );
  }

  private function buildDescription( $document, $position )
  {
    $parts = [
      'Fakturownia'
    ];

    if ( $document['number'] !== '' )
      $parts[] = 'FV: ' . $document['number'];

    $parts[] = $position['name'];
    $parts[] = $document['client_name'];

    return implode( ' | ', $parts );
  }

  private function resolveConfig()
  {
    $domain = trim( (string)\Env::get( 'FAKTUROWNIA_API_DOMAIN', '' ) );
    $token = trim( (string)\Env::get( 'FAKTUROWNIA_API_TOKEN', '' ) );
    $startDate = trim( (string)\Env::get( 'FAKTUROWNIA_START_DATE', '' ) );
    $pageLimit = (int)\Env::get( 'FAKTUROWNIA_PAGE_LIMIT', 100 );
    $ownTaxNo = trim( (string)\Env::get( 'FAKTUROWNIA_OWN_TAX_NO', '' ) );

    if ( $domain === '' || $token === '' || $startDate === '' )
      return [ 'status' => 'error', 'ok' => false, 'msg' => 'Import Fakturownia: brak konfiguracji w .env.' ];

    if ( !preg_match( '/^\d{4}-\d{2}-\d{2}$/', $startDate ) )
      return [ 'status' => 'error', 'ok' => false, 'msg' => 'Import Fakturownia: nieprawidlowa data FAKTUROWNIA_START_DATE.' ];

    if ( strpos( $domain, 'http://' ) !== 0 && strpos( $domain, 'https://' ) !== 0 )
      $domain = 'https://' . $domain;

    return [
      'ok' => true,
      'api_url' => rtrim( $domain, '/' ),
      'token' => $token,
      'start_date' => $startDate,
      'page_limit' => $pageLimit > 0 ? $pageLimit : 100,
      'own_tax_no' => $ownTaxNo
    ];
  }

  private function formatMessage( $summary )
  {
    return 'Import Fakturownia: zaimportowano ' . (int)$summary['imported']
      . ', pominieto ' . (int)$summary['skipped']
      . ', brak mapowan ' . (int)$summary['unmapped']
      . ', bledy ' . (int)$summary['errors'] . '.';
  }
}
