<?php
/**
 * Central finance/payment helpers.
 * Keeps existing page behaviour, but makes invoice payment / unpayment / manual payment rows use one source.
 */

require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/db_bootstrap.php';
require_once __DIR__ . '/mikrotik_lib.php';
require_once __DIR__ . '/billing_cycle_lib.php';
require_once __DIR__ . '/billing_account_lib.php';
require_once __DIR__ . '/app_runtime.php';
require_once __DIR__ . '/agent_service.php';

if (!function_exists('finance_payment_admin_name')) {
  function finance_payment_admin_name(): string {
    return (string)($_SESSION['admin_username'] ?? ($_SESSION['admin_user'] ?? 'admin'));
  }
}

if (!function_exists('finance_ensure_payments_table')) {
  function finance_ensure_payments_table(mysqli $conn): void {
    $required = ['reference','ref','agent_id','paid_at','source_instance_id','idempotency_key','reversed_payment_id','payment_status','collection_source_type','collection_source_ref','collection_source_name','collection_channel'];
    $isCurrent = function_exists('table_exists') && table_exists($conn, 'payments');
    if ($isCurrent && function_exists('col_exists')) {
      foreach ($required as $column) {
        if (!col_exists($conn, 'payments', $column)) {
          $isCurrent = false;
          break;
        }
      }
    }
    if ($isCurrent) {
      billing_accounts_migrate($conn);
      return;
    }

    @ $conn->query("CREATE TABLE IF NOT EXISTS `payments` (
      `id` INT AUTO_INCREMENT PRIMARY KEY,
      `invoice_id` INT DEFAULT NULL,
      `subscriber_id` INT DEFAULT NULL,
      `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
      `method` VARCHAR(50) DEFAULT NULL,
      `ref` VARCHAR(120) DEFAULT NULL,
      `reference` VARCHAR(120) DEFAULT NULL,
      `note` VARCHAR(255) DEFAULT NULL,
      `created_by` VARCHAR(100) DEFAULT NULL,
      `agent_id` INT DEFAULT NULL,
      `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `paid_at` DATETIME DEFAULT NULL,
      INDEX (`invoice_id`), INDEX (`subscriber_id`), INDEX (`agent_id`), INDEX (`created_at`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

    if (function_exists('table_exists') && table_exists($conn, 'payments')) {
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'reference')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `reference` VARCHAR(120) NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'ref')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `ref` VARCHAR(120) NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'agent_id')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `agent_id` INT NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'paid_at')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `paid_at` DATETIME NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'source_instance_id')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `source_instance_id` VARCHAR(80) NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'idempotency_key')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `idempotency_key` VARCHAR(191) NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'reversed_payment_id')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `reversed_payment_id` INT NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'payment_status')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `payment_status` VARCHAR(20) NOT NULL DEFAULT 'POSTED'");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'collection_source_type')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `collection_source_type` VARCHAR(24) NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'collection_source_ref')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `collection_source_ref` VARCHAR(100) NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'collection_source_name')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `collection_source_name` VARCHAR(150) NULL");
      if (function_exists('col_exists') && !col_exists($conn, 'payments', 'collection_channel')) @ $conn->query("ALTER TABLE `payments` ADD COLUMN `collection_channel` VARCHAR(24) NULL");
      @ $conn->query("CREATE INDEX idx_payments_agent_id ON payments(agent_id)");
      @ $conn->query("CREATE UNIQUE INDEX uq_payments_idempotency ON payments(idempotency_key)");
      @ $conn->query("UPDATE payments SET paid_at = COALESCE(paid_at, created_at) WHERE paid_at IS NULL");
      @ $conn->query("UPDATE payments SET reference = COALESCE(reference, ref) WHERE reference IS NULL");
      @ $conn->query("UPDATE payments SET ref = COALESCE(ref, reference) WHERE ref IS NULL");
      @ $conn->query("UPDATE payments SET payment_status = 'POSTED' WHERE payment_status IS NULL OR payment_status = ''");
    }
    billing_accounts_migrate($conn);
  }
}

if (!function_exists('finance_default_collection_context')) {
  function finance_default_collection_context(array $options = []): array {
    $installType = function_exists('ua_install_type') ? ua_install_type() : 'server';
    $sourceType = strtolower(trim((string)($options['source_type'] ?? ($installType === 'branch' ? 'branch' : ($installType === 'agent' ? 'agent' : 'main')))));
    if (!in_array($sourceType, ['main','branch','agent'], true)) $sourceType = 'main';
    $sourceInstance = trim((string)($options['source_instance_id'] ?? ''));
    if ($sourceInstance === '' && function_exists('ua_install_instance_key')) $sourceInstance = ua_install_instance_key();
    $sourceRef = trim((string)($options['source_ref'] ?? ''));
    if ($sourceRef === '') {
      if ($sourceType === 'agent' && !empty($options['agent_id'])) $sourceRef = (string)(int)$options['agent_id'];
      elseif ($sourceType === 'main') $sourceRef = 'main';
      else $sourceRef = $sourceInstance;
    }
    $defaultName = $sourceType === 'branch' ? 'نسخة فرعية' : ($sourceType === 'agent' ? 'نسخة معتمد' : 'النسخة الرئيسية');
    return [
      'source_type'=>$sourceType,
      'source_ref'=>substr($sourceRef, 0, 100),
      'source_name'=>substr(trim((string)($options['source_name'] ?? $defaultName)), 0, 150),
      'source_instance_id'=>substr($sourceInstance, 0, 100),
      'channel'=>substr(trim((string)($options['channel'] ?? 'desktop')), 0, 24),
    ];
  }
}

if (!function_exists('finance_fetch_invoice_with_subscriber')) {
  function finance_fetch_invoice_with_subscriber(mysqli $conn, int $invoiceId): ?array {
    $invoiceId = (int)$invoiceId;
    if ($invoiceId <= 0) return null;
    $sql = "
      SELECT i.*,
             s.id AS sid,
             s.full_name,
             s.status AS sub_status,
             s.ppp_username,
             s.ppp_password,
             s.ppp_profile,
             s.ppp_rate_limit,
             s.speed_name
      FROM invoices i
      LEFT JOIN subscribers s ON i.subscriber_id=s.id
      WHERE i.id={$invoiceId}
      LIMIT 1
    ";
    $res = @ $conn->query($sql);
    return ($res && $res->num_rows) ? $res->fetch_assoc() : null;
  }
}

if (!function_exists('finance_ar_paid_status')) {
  function finance_ar_paid_status(): string {
    return "\xD9\x85\xD8\xAF\xD9\x81\xD9\x88\xD8\xB9";
  }
}

if (!function_exists('finance_invoice_is_paid')) {
  function finance_invoice_is_paid(array $invoice): bool {
    $paymentStatus = strtolower(trim((string)($invoice['payment_status'] ?? '')));
    $status = trim((string)($invoice['status'] ?? ''));
    return in_array($paymentStatus, ['paid','settled'], true)
      || $status === finance_ar_paid_status()
      || strtolower($status) === 'paid';
  }
}

if (!function_exists('finance_invoice_payment_due')) {
  function finance_invoice_payment_due(array $invoice, ?string $today = null): bool {
    $dueDate = trim((string)($invoice['due_date'] ?? ''));
    if ($dueDate === '' || $dueDate === '0000-00-00') return true;
    $dueTs = strtotime($dueDate . ' 00:00:00');
    $todayTs = strtotime(($today ?: date('Y-m-d')) . ' 00:00:00');
    return $dueTs === false || $todayTs === false || $todayTs >= $dueTs;
  }
}

if (!function_exists('finance_payment_message_enabled')) {
  function finance_payment_message_enabled(mysqli $conn): bool {
    return get_app_setting($conn, 'invoice_send_paid_message', '0') === '1';
  }
}

if (!function_exists('finance_payment_auto_print_enabled')) {
  function finance_payment_auto_print_enabled(mysqli $conn): bool {
    return get_app_setting($conn, 'invoice_auto_print_on_payment', '0') === '1';
  }
}

if (!function_exists('finance_render_auto_print')) {
  function finance_render_auto_print(mysqli $conn, int $invoiceId, int $agentId = 0): void {
    if ($invoiceId <= 0 || !finance_payment_auto_print_enabled($conn)) return;
    $paper = '80mm';
    if (table_exists($conn, 'print_settings')) {
      $paperRs = @$conn->query("SELECT invoice_paper FROM print_settings WHERE id=1 LIMIT 1");
      $paperRow = $paperRs ? $paperRs->fetch_assoc() : null;
      $savedPaper = trim((string)($paperRow['invoice_paper'] ?? ''));
      if (in_array($savedPaper, ['A4','80mm','58mm'], true)) $paper = $savedPaper;
    }
    require_once __DIR__ . '/output_devices_lib.php';
    $baseScope = $agentId > 0 ? ua_output_agent_scope($agentId) : ua_output_scope_key();
    $preferredCommand = $paper === 'A4' ? 'invoice_a4' : 'invoice_thermal';
    $config = [
      'invoiceId'=>$invoiceId,
      'agentId'=>$agentId,
      'paper'=>$paper,
      'baseScope'=>$baseScope,
      'commands'=>[$preferredCommand, $preferredCommand === 'invoice_a4' ? 'invoice_thermal' : 'invoice_a4'],
      'configUrl'=>'output_route_config.php',
      'printUrl'=>'print_invoice.php',
    ];
    $json = json_encode($config, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT);
    echo '<script>(function(c){';
    echo 'function deviceId(){var k="ua_output_device_id_v1",v="";try{v=String(localStorage.getItem(k)||"").toLowerCase()}catch(e){}if(!/^[a-z0-9][a-z0-9_-]{7,63}$/.test(v)){v=(window.crypto&&crypto.randomUUID)?crypto.randomUUID().toLowerCase():"device-"+Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,12);try{localStorage.setItem(k,v)}catch(e){}}return v}';
    echo 'function launch(p){var q=new URLSearchParams({id:String(c.invoiceId),paper:p,auto:"1",silent:"1"});if(c.agentId>0)q.set("agent",String(c.agentId));var f=document.createElement("iframe");f.title="طباعة الفاتورة تلقائياً";f.setAttribute("aria-hidden","true");f.style.cssText="position:fixed;width:1px;height:1px;opacity:0;pointer-events:none;border:0";f.src=c.printUrl+"?"+q.toString();document.body.appendChild(f)}';
    echo 'async function run(){var scope=c.baseScope+":device:"+deviceId();for(var i=0;i<c.commands.length;i++){try{var u=new URL(c.configUrl,location.href);u.searchParams.set("command_key",c.commands[i]);u.searchParams.set("scope_key",scope);var r=await fetch(u,{cache:"no-store",credentials:"same-origin"});var j=await r.json();if(r.ok&&j.ok&&j.route&&j.route.enabled&&j.route.deviceName){launch(String(j.route.paperSize||c.paper));return}}catch(e){}}launch(c.paper)}run();';
    echo '})(' . $json . ');</script>';
  }
}

if (!function_exists('finance_enqueue_paid_message')) {
  function finance_enqueue_paid_message(mysqli $conn, array $invoice, int $invoiceId, float $amount): array {
    if (!finance_payment_message_enabled($conn)) return ['enabled'=>false, 'queued'=>false, 'warning'=>''];
    $queued = billing_enqueue_template_message($conn, 'INVOICE_PAID', (string)($invoice['phone'] ?? ''), [
      'invoice_id'=>$invoiceId,
      'amount'=>number_format($amount, 2, '.', ''),
      'remaining_amount'=>'0.00',
      'cycle_month'=>(string)($invoice['cycle_month'] ?? ''),
    ], (int)($invoice['sid'] ?? $invoice['subscriber_id'] ?? 0));
    return [
      'enabled'=>true,
      'queued'=>$queued,
      'warning'=>$queued ? '' : 'تم التسديد، لكن تعذر إدراج رسالة تأكيد التسديد في طابور الإرسال.',
    ];
  }
}

if (!function_exists('finance_apply_paid_addon_invoice')) {
  function finance_apply_paid_addon_invoice(mysqli $conn, array $invoice, int $invoiceId): array {
    $invoiceType = strtoupper(trim((string)($invoice['invoice_type'] ?? $invoice['type'] ?? '')));
    if ($invoiceType !== 'ADDON') return ['ok'=>true,'warning'=>''];

    $sid = (int)($invoice['sid'] ?? $invoice['subscriber_id'] ?? 0);
    if ($sid <= 0) return ['ok'=>false,'warning'=>'تم التسديد لكن لم يتم تفعيل الباقة الإضافية لأن المشترك غير محدد.'];

    $addonGb = (int)preg_replace('/[^0-9]/', '', (string)($invoice['addon_size'] ?? ''));
    if ($addonGb <= 0) return ['ok'=>false,'warning'=>'تم التسديد لكن لم يتم تفعيل الباقة الإضافية لأن حجم الباقة غير محدد.'];

    $cycleMonth = trim((string)($invoice['cycle_month'] ?? date('Y-m')));
    if (!preg_match('/^\d{4}\-\d{2}$/', $cycleMonth)) $cycleMonth = date('Y-m');

    require_once __DIR__ . '/quota_lib.php';
    q_ensure_tables($conn);

    $sub = null;
    $q = @ $conn->query("SELECT id, username, ppp_username, base_speed, ppp_rate_limit FROM subscribers WHERE id={$sid} LIMIT 1");
    if ($q && $q->num_rows) $sub = $q->fetch_assoc();
    if (!$sub) return ['ok'=>false,'warning'=>'تم التسديد لكن لم يتم تفعيل الباقة الإضافية لأن بيانات المشترك غير موجودة.'];

    $pppUser = trim((string)($sub['ppp_username'] ?? ''));
    $quotaUser = $pppUser !== '' ? $pppUser : trim((string)($sub['username'] ?? ''));
    if ($quotaUser === '') return ['ok'=>false,'warning'=>'تم التسديد لكن لم يتم تفعيل الباقة الإضافية لأن اسم PPPoE غير موجود.'];

    q_add_addon($conn, $sid, $quotaUser, $cycleMonth, $addonGb, $invoiceId);

    $baseRate = trim((string)($sub['base_speed'] ?? ''));
    if ($baseRate === '') $baseRate = trim((string)($sub['ppp_rate_limit'] ?? ''));
    if ($pppUser !== '' && $baseRate !== '') {
      try {
        mt_ppp_secret_set_rate($conn, $pppUser, $baseRate);
        mt_ppp_active_disconnect($conn, $pppUser);
      } catch (Throwable $e) {
        return ['ok'=>true,'warning'=>'تم تفعيل الباقة الإضافية، لكن فشل تحديث السرعة أو إعادة الاتصال على MikroTik.'];
      }
    }

    return ['ok'=>true,'warning'=>''];
  }
}

if (!function_exists('finance_set_invoice_paid_state')) {
  function finance_set_invoice_paid_state(mysqli $conn, int $invoiceId, float $amount, string $paidAt, string $paidByType = 'admin', ?int $paidById = null): array {
    $invoiceId = (int)$invoiceId;
    if ($invoiceId <= 0) return ['ok'=>false,'error'=>'رقم الفاتورة غير صحيح'];
    $sets = [];
    if (col_exists($conn,'invoices','status')) $sets[] = "status='مدفوع'";
    if (col_exists($conn,'invoices','paid_at')) $sets[] = "paid_at='".$conn->real_escape_string($paidAt)."'";
    if (col_exists($conn,'invoices','paid_amount')) $sets[] = "paid_amount=".(float)$amount;
    if (col_exists($conn,'invoices','remaining_amount')) $sets[] = "remaining_amount=0.00";
    if (col_exists($conn,'invoices','payment_status')) $sets[] = "payment_status='paid'";
    if (col_exists($conn,'invoices','paid_by_type')) $sets[] = "paid_by_type='".$conn->real_escape_string($paidByType)."'";
    if (col_exists($conn,'invoices','paid_by_id')) $sets[] = $paidById ? "paid_by_id=".(int)$paidById : "paid_by_id=NULL";
    if (col_exists($conn,'invoices','updated_at')) $sets[] = "updated_at='".$conn->real_escape_string($paidAt)."'";
    if ($sets && !@ $conn->query("UPDATE invoices SET ".implode(',', $sets)." WHERE id={$invoiceId}")) {
      return ['ok'=>false,'error'=>'DB Error: '.$conn->error];
    }
    return ['ok'=>true,'error'=>''];
  }
}

if (!function_exists('finance_set_invoice_unpaid_state')) {
  function finance_set_invoice_unpaid_state(mysqli $conn, int $invoiceId): array {
    $invoiceId = (int)$invoiceId;
    if ($invoiceId <= 0) return ['ok'=>false,'error'=>'رقم الفاتورة غير صحيح'];
    $sets = [];
    if (col_exists($conn,'invoices','status')) $sets[] = "status='غير مدفوع'";
    if (col_exists($conn,'invoices','paid_at')) $sets[] = "paid_at=NULL";
    if (col_exists($conn,'invoices','paid_amount')) $sets[] = "paid_amount=0.00";
    if (col_exists($conn,'invoices','remaining_amount') && col_exists($conn,'invoices','amount')) $sets[] = "remaining_amount=amount";
    if (col_exists($conn,'invoices','payment_status')) $sets[] = "payment_status='unpaid'";
    if (col_exists($conn,'invoices','paid_by_type')) $sets[] = "paid_by_type=NULL";
    if (col_exists($conn,'invoices','paid_by_id')) $sets[] = "paid_by_id=NULL";
    if (col_exists($conn,'invoices','updated_at')) $sets[] = "updated_at=NOW()";
    if (!$sets) return ['ok'=>true,'error'=>''];
    if (!@ $conn->query("UPDATE invoices SET ".implode(',', $sets)." WHERE id={$invoiceId} LIMIT 1")) {
      return ['ok'=>false,'error'=>'DB Error: '.$conn->error];
    }
    return ['ok'=>true,'error'=>''];
  }
}

if (!function_exists('finance_record_payment_row')) {
  function finance_record_payment_row(mysqli $conn, int $invoiceId, int $subscriberId, float $amount, string $method, string $note, string $createdBy, string $paidAt, string $reference = '', ?int $agentId = null, array $options = []): array {
    finance_ensure_payments_table($conn);
    ua_agent_schema_migrate($conn);
    if (!table_exists($conn,'payments')) return ['ok'=>true,'error'=>''];
    $idempotencyKey = substr(trim((string)($options['idempotency_key'] ?? '')), 0, 191);
    $sourceInstanceId = substr(trim((string)($options['source_instance_id'] ?? '')), 0, 80);
    $collection = finance_default_collection_context($options + ['agent_id'=>$agentId, 'source_instance_id'=>$sourceInstanceId]);
    require_once __DIR__ . '/finance_daily_cashbox_lib.php';
    $dailySource = ['type'=>$collection['source_type'],'ref'=>$collection['source_ref'],'name'=>$collection['source_name']];
    if (finance_daily_account_closed($conn, date('Y-m-d', strtotime($paidAt)), $dailySource)) {
      return ['ok'=>false,'error'=>'صندوق هذه النسخة مقفل لهذا اليوم، ولا يمكن تسجيل دفعة جديدة فيه.'];
    }
    if ($idempotencyKey !== '') {
      $keyEsc = $conn->real_escape_string($idempotencyKey);
      $old = @$conn->query("SELECT id,invoice_id,agent_id,payment_status FROM payments WHERE idempotency_key='{$keyEsc}' LIMIT 1");
      if ($old && $old->num_rows) {
        return ['ok'=>true,'duplicate'=>true,'id'=>(int)$old->fetch_assoc()['id'],'error'=>''];
      }
    }
    $stmt = $conn->prepare("INSERT INTO payments
      (invoice_id,subscriber_id,amount,method,reference,ref,note,created_by,agent_id,source_instance_id,idempotency_key,payment_status,paid_at,created_at,collection_source_type,collection_source_ref,collection_source_name,collection_channel)
      VALUES (?,?,?,?,?,?,?,?,?,?,?,'POSTED',?,?,?,?,?,?)");
    if (!$stmt) return ['ok'=>false,'error'=>'DB Error: '.$conn->error];
    $invoiceDb = $invoiceId > 0 ? $invoiceId : null;
    $subscriberDb = $subscriberId > 0 ? $subscriberId : null;
    $agentDb = ($agentId !== null && $agentId > 0) ? (int)$agentId : null;
    $sourceType = (string)$collection['source_type'];
    $sourceRef = (string)$collection['source_ref'];
    $sourceName = (string)$collection['source_name'];
    $channel = (string)$collection['channel'];
    $stmt->bind_param('iidsssssissssssss', $invoiceDb, $subscriberDb, $amount, $method, $reference, $reference, $note, $createdBy, $agentDb, $sourceInstanceId, $idempotencyKey, $paidAt, $paidAt, $sourceType, $sourceRef, $sourceName, $channel);
    $ok = @ $stmt->execute();
    $error = $ok ? '' : ('فشل إضافة الدفعة: ' . $stmt->error);
    $paymentId = $ok ? (int)$stmt->insert_id : 0;
    $stmt->close();
    if ($ok) {
      $posted = billing_record_collection_payment($conn, $paymentId, $invoiceId, $subscriberId, $amount, $collection + [
        'idempotency_key'=>$idempotencyKey !== '' ? $idempotencyKey : ('PAYMENT:' . $paymentId),
        'note'=>$note,
      ]);
      if (empty($posted['ok'])) {
        @ $conn->query("DELETE FROM payments WHERE id={$paymentId} LIMIT 1");
        return ['ok'=>false,'duplicate'=>false,'id'=>0,'error'=>(string)($posted['error'] ?? 'فشل تسجيل حساب التحصيل')];
      }
    }
    if ($ok && $invoiceId > 0 && function_exists('finance_recalc_invoice')) finance_recalc_invoice($conn, $invoiceId);
    return ['ok'=>$ok,'duplicate'=>false,'id'=>$paymentId,'error'=>$error,'collection'=>$posted ?? []];
  }
}

if (!function_exists('finance_delete_payment_row')) {
  function finance_delete_payment_row(mysqli $conn, int $paymentId): array {
    $paymentId = (int)$paymentId;
    if ($paymentId <= 0) return ['ok'=>false,'error'=>'رقم الدفعة غير صحيح','invoice_id'=>0];
    ua_agent_schema_migrate($conn);
    $invId = 0;
    try {
      $conn->begin_transaction();
      $rsInv = @$conn->query("SELECT * FROM payments WHERE id={$paymentId} LIMIT 1 FOR UPDATE");
      if (!$rsInv || !$rsInv->num_rows) throw new RuntimeException('الدفعة غير موجودة.');
      $payment = $rsInv->fetch_assoc();
      $invId = (int)($payment['invoice_id'] ?? 0);
      $agentId = (int)($payment['agent_id'] ?? 0);
      require_once __DIR__ . '/finance_daily_cashbox_lib.php';
      $paymentSource = finance_daily_source_context([
        'source_type'=>$payment['collection_source_type'] ?? ($agentId > 0 ? 'agent' : 'main'),
        'source_ref'=>$payment['collection_source_ref'] ?? '',
        'source_name'=>$payment['collection_source_name'] ?? '',
        'source_instance_id'=>$payment['source_instance_id'] ?? '',
      ]);
      $paymentDay = date('Y-m-d', strtotime((string)($payment['paid_at'] ?? $payment['created_at'] ?? 'now')));
      if (finance_daily_account_closed($conn, $paymentDay, $paymentSource)) throw new RuntimeException('لا يمكن إلغاء دفعة من صندوق نسخة مغلق.');
      $collectionReverse = billing_reverse_collection_payment($conn, $paymentId);
      if (empty($collectionReverse['ok'])) throw new RuntimeException((string)($collectionReverse['error'] ?? 'فشل عكس رصيد التحصيل'));

      if ($agentId > 0) {
        $entryRs = @$conn->query("SELECT id FROM agent_ledger_entries WHERE payment_id={$paymentId} AND status='POSTED' ORDER BY id DESC LIMIT 1");
        if ($entryRs && ($entry = $entryRs->fetch_assoc())) {
          $reversed = ua_agent_ledger_reverse($conn, (int)$entry['id'], 'إلغاء الدفعة رقم ' . $paymentId, ['type'=>'admin'], false);
          if (!$reversed['ok']) throw new RuntimeException($reversed['error']);
        }
        if (!@$conn->query("UPDATE payments SET payment_status='REVERSED' WHERE id={$paymentId}")) {
          throw new RuntimeException($conn->error);
        }
      } else {
        if (!@$conn->query("UPDATE payments SET payment_status='REVERSED' WHERE id={$paymentId} LIMIT 1")) {
          throw new RuntimeException($conn->error);
        }
      }
      $conn->commit();
      $ok = true;
      $error = '';
    } catch (Throwable $e) {
      @$conn->rollback();
      $ok = false;
      $error = 'فشل إلغاء الدفعة: ' . $e->getMessage();
    }
    if ($ok && $invId > 0 && function_exists('finance_recalc_invoice')) finance_recalc_invoice($conn, $invId);
    return ['ok'=>$ok,'error'=>$error,'invoice_id'=>$invId];
  }
}

if (!function_exists('finance_restore_subscriber_after_invoice_payment')) {
  function finance_restore_subscriber_after_invoice_payment(mysqli $conn, array $invoice, int $invoiceId): array {
    $sid = (int)($invoice['sid'] ?? $invoice['subscriber_id'] ?? 0);
    if ($sid <= 0) return ['ok'=>true,'warning'=>''];

    $canRestoreService = billing_restore_subscriber_after_payment($conn, $sid, $invoiceId);
    $addon = finance_apply_paid_addon_invoice($conn, $invoice, $invoiceId);
    $addonWarning = (string)($addon['warning'] ?? '');
    if (!$canRestoreService && strtoupper(trim((string)($invoice['invoice_type'] ?? 'BASE'))) === 'BASE') {
      $pauseRs = @$conn->query("SELECT COALESCE(billing_paused,0) billing_paused FROM subscribers WHERE id={$sid} LIMIT 1");
      $pauseRow = $pauseRs ? $pauseRs->fetch_assoc() : null;
      $holdReason = (int)($pauseRow['billing_paused'] ?? 0) === 1
        ? ' تم تسجيل التسديد مع بقاء حساب المشترك متوقفاً مؤقتاً حتى إعادة تفعيله يدوياً.'
        : ' بقيت الخدمة على حالتها لأن لدى المشترك فواتير أخرى غير مسددة.';
      return ['ok'=>true,'warning'=>trim($addonWarning . $holdReason)];
    }

    $q = @ $conn->query("SELECT ppp_username, ppp_password, ppp_profile, ppp_rate_limit, speed_name FROM subscribers WHERE id={$sid} LIMIT 1");
    if (!($q && $q->num_rows)) return ['ok'=>true,'warning'=>$addonWarning];

    $sub = $q->fetch_assoc();
    $pppUser = trim((string)($sub['ppp_username'] ?? ''));
    $pppPass = trim((string)($sub['ppp_password'] ?? ''));
    if ($pppUser === '' || $pppPass === '') return ['ok'=>true,'warning'=>$addonWarning];

    $profile = trim((string)($sub['ppp_profile'] ?? ''));
    $rate = trim((string)($sub['ppp_rate_limit'] ?? ''));
    if ($rate === '') {
      $sn = trim((string)($sub['speed_name'] ?? ''));
      if ($sn !== '') $rate = $sn . '/' . $sn;
    }

    $ok = mt_ppp_secret_upsert($conn, $pppUser, $pppPass, $profile, '', 'AUTO-PAID:'.$invoiceId);
    if (!$ok) {
      return ['ok'=>false,'warning'=>trim($addonWarning . ' تم التسديد لكن فشل تحديث PPP على MikroTik للمستخدم: ' . $pppUser)];
    }

    mt_ppp_secret_set_disabled($conn, $pppUser, false);
    $subUiSets = [];
    if (col_exists($conn,'subscribers','mt_disabled')) $subUiSets[] = "mt_disabled=0";
    if (col_exists($conn,'subscribers','ppp_is_disabled')) $subUiSets[] = "ppp_is_disabled=0";
    if (col_exists($conn,'subscribers','ppp_disabled_at')) $subUiSets[] = "ppp_disabled_at=NULL";
    if ($subUiSets) @ $conn->query("UPDATE subscribers SET " . implode(',', $subUiSets) . " WHERE id={$sid}");
    if ($rate !== '') mt_ppp_secret_set_rate($conn, $pppUser, $rate);

    return ['ok'=>true,'warning'=>$addonWarning];
  }
}

if (!function_exists('finance_pay_invoice')) {
  function finance_pay_invoice(mysqli $conn, int $invoiceId, array $options = []): array {
    $invoiceId = (int)$invoiceId;
    if ($invoiceId <= 0) return ['ok'=>false,'error'=>'رقم الفاتورة غير صحيح','warning'=>''];

    $paidAt = (string)($options['paid_at'] ?? date('Y-m-d H:i:s'));
    $method = (string)($options['method'] ?? 'نقدي');
    $note = (string)($options['note'] ?? 'تسديد فاتورة من صفحة السداد');
    $by = (string)($options['created_by'] ?? finance_payment_admin_name());
    $reference = (string)($options['reference'] ?? '');
    $agentId = isset($options['agent_id']) ? (int)$options['agent_id'] : null;
    $collection = finance_default_collection_context($options + ['agent_id'=>$agentId]);
    $idempotencyKey = substr(trim((string)($options['idempotency_key'] ?? '')), 0, 191);
    if ($idempotencyKey === '') $idempotencyKey = 'INVOICE_PAYMENT:' . $invoiceId . ':' . ($reference !== '' ? $reference : ua_agent_uuid());

    ua_agent_schema_migrate($conn);
    billing_accounts_migrate($conn);
    $paymentId = 0;
    $advancePaymentId = 0;
    $fullyPaid = false;
    $remainingAfter = 0.0;
    $invoicePayment = 0.0;
    $advanceAmount = 0.0;
    try {
      $conn->begin_transaction();
      $locked = @$conn->query("SELECT i.*,s.id sid,s.full_name,s.phone,s.ppp_username,s.ppp_password,s.ppp_profile,s.ppp_rate_limit,s.speed_name
        FROM invoices i LEFT JOIN subscribers s ON i.subscriber_id=s.id WHERE i.id={$invoiceId} LIMIT 1 FOR UPDATE");
      if (!$locked || !$locked->num_rows) throw new RuntimeException('الفاتورة غير موجودة.');
      $inv = $locked->fetch_assoc();
      if (finance_invoice_is_paid($inv)) throw new RuntimeException('هذه الفاتورة مدفوعة مسبقا.');
      if (!finance_invoice_payment_due($inv)) throw new RuntimeException('لا يمكن تسديد الفاتورة قبل تاريخ الاستحقاق.');

      $credit = billing_apply_credit_to_invoice($conn, $invoiceId);
      if (empty($credit['ok'])) throw new RuntimeException((string)($credit['error'] ?? 'تعذر تطبيق الرصيد المقدم'));
      $fresh = @$conn->query("SELECT amount,paid_amount,remaining_amount,payment_status,status FROM invoices WHERE id={$invoiceId} LIMIT 1 FOR UPDATE");
      $invoiceState = $fresh ? $fresh->fetch_assoc() : [];
      if (finance_invoice_is_paid($invoiceState)) {
        $conn->commit();
        $restore = finance_restore_subscriber_after_invoice_payment($conn, $inv, $invoiceId);
        $messageResult = finance_enqueue_paid_message($conn, $inv, $invoiceId, (float)($inv['amount'] ?? 0));
        return [
          'ok'=>true,'payment_id'=>0,'credit_only'=>true,'error'=>'',
          'warning'=>trim('تم تسديد الفاتورة بالكامل من الرصيد المقدم. ' . (string)($restore['warning'] ?? '') . ' ' . (string)$messageResult['warning']),
          'message_queued'=>!empty($messageResult['queued']),
          'auto_print'=>finance_payment_auto_print_enabled($conn),
        ];
      }
      $remaining = round((float)($invoiceState['remaining_amount'] ?? $inv['amount'] ?? 0), 2);
      $received = round((float)($options['amount'] ?? $remaining), 2);
      if ($received <= 0) throw new RuntimeException('قيمة الدفعة غير صحيحة.');
      $invoicePayment = round(min($received, $remaining), 2);
      $advanceAmount = round(max(0, $received - $invoicePayment), 2);

      $rec = finance_record_payment_row($conn, $invoiceId, (int)($inv['sid'] ?? 0), $invoicePayment, $method, $note, $by, $paidAt, $reference, $agentId, [
        'idempotency_key'=>$idempotencyKey,
        'source_instance_id'=>$collection['source_instance_id'],
        'source_type'=>$collection['source_type'],
        'source_ref'=>$collection['source_ref'],
        'source_name'=>$collection['source_name'],
        'channel'=>$collection['channel'],
      ]);
      if (!$rec['ok']) throw new RuntimeException($rec['error']);
      $paymentId = (int)($rec['id'] ?? 0);

      if ($agentId) {
        $ledger = ua_agent_service_post_charge($conn, [
          'agent_id'=>$agentId,
          'entry_type'=>'INVOICE_PAYMENT',
          'amount'=>$invoicePayment,
          'subscriber_id'=>(int)($inv['sid'] ?? 0),
          'invoice_id'=>$invoiceId,
          'payment_id'=>$paymentId,
          'source_type'=>'payment',
          'source_id'=>(string)$paymentId,
          'source_instance_id'=>$collection['source_instance_id'],
          'idempotency_key'=>'LEDGER:' . $idempotencyKey,
          'description'=>$note,
          'created_by_type'=>'agent',
          'created_by_id'=>$agentId,
        ], false);
        if (!$ledger['ok']) throw new RuntimeException($ledger['error']);
        ua_agent_activity_add($conn, [
          'agent_id'=>$agentId,
          'event_type'=>'INVOICE_PAID',
          'direction'=>'TO_MAIN',
          'entity_type'=>'invoice',
          'entity_id'=>(string)$invoiceId,
          'request_id'=>$idempotencyKey,
          'summary'=>'تم تسديد الفاتورة وترحيلها إلى رصيد المعتمد.',
          'payload'=>['payment_id'=>$paymentId,'amount'=>$invoicePayment],
          'actor_type'=>'agent',
          'actor_id'=>$agentId,
        ]);
      }
      if ($advanceAmount > 0) {
        $advanceRec = finance_record_payment_row($conn, 0, (int)($inv['sid'] ?? 0), $advanceAmount, $method, 'دفعة مقدمة مرافقة لتسديد الفاتورة ' . $invoiceId, $by, $paidAt, $reference . '-ADV', $agentId, [
          'idempotency_key'=>$idempotencyKey . ':ADVANCE',
          'source_instance_id'=>$collection['source_instance_id'],
          'source_type'=>$collection['source_type'],
          'source_ref'=>$collection['source_ref'],
          'source_name'=>$collection['source_name'],
          'channel'=>$collection['channel'],
        ]);
        if (empty($advanceRec['ok'])) throw new RuntimeException((string)($advanceRec['error'] ?? 'فشل تسجيل الدفعة المقدمة'));
        $advancePaymentId = (int)($advanceRec['id'] ?? 0);
        $creditPost = billing_credit_post($conn, (int)($inv['sid'] ?? 0), 'CREDIT', $advanceAmount, [
          'entry_type'=>'ADVANCE_PAYMENT',
          'payment_id'=>$advancePaymentId,
          'source_instance_id'=>$collection['source_instance_id'],
          'idempotency_key'=>'CREDIT:' . $idempotencyKey . ':ADVANCE',
          'note'=>'رصيد إضافي بعد تسديد الفاتورة رقم ' . $invoiceId,
          'created_by_type'=>$collection['source_type'],
          'created_by_id'=>$agentId,
        ]);
        if (empty($creditPost['ok'])) throw new RuntimeException((string)($creditPost['error'] ?? 'فشل إضافة الرصيد المقدم'));
      }
      finance_recalc_invoice($conn, $invoiceId);
      $stateRs = @$conn->query("SELECT remaining_amount,payment_status FROM invoices WHERE id={$invoiceId} LIMIT 1");
      $state = $stateRs ? $stateRs->fetch_assoc() : [];
      $remainingAfter = round((float)($state['remaining_amount'] ?? 0), 2);
      $fullyPaid = $remainingAfter <= 0.00001 || strtolower((string)($state['payment_status'] ?? '')) === 'paid';
      if ($fullyPaid) {
        $paidByType = $collection['source_type'] === 'main' ? 'admin' : $collection['source_type'];
        $sets = ["paid_at='".$conn->real_escape_string($paidAt)."'", "paid_by_type='".$conn->real_escape_string($paidByType)."'"];
        $sets[] = $agentId ? "paid_by_id=".(int)$agentId : "paid_by_id=NULL";
        @ $conn->query("UPDATE invoices SET " . implode(',', $sets) . " WHERE id={$invoiceId} LIMIT 1");
      }
      $conn->commit();
    } catch (Throwable $e) {
      @$conn->rollback();
      return ['ok'=>false,'error'=>$e->getMessage(),'warning'=>''];
    }

    $warning = '';
    if ($fullyPaid) {
      $restore = finance_restore_subscriber_after_invoice_payment($conn, $inv, $invoiceId);
      $warning = (string)($restore['warning'] ?? '');
      $messageResult = finance_enqueue_paid_message($conn, $inv, $invoiceId, $invoicePayment);
      if (!empty($messageResult['warning'])) $warning = trim($warning . ' ' . (string)$messageResult['warning']);
    } else {
      @ $conn->query("UPDATE subscribers SET billing_state='DUE',billing_state_updated_at=NOW() WHERE id=".(int)($inv['sid'] ?? 0));
    }
    return [
      'ok'=>true,
      'payment_id'=>$paymentId,
      'advance_payment_id'=>$advancePaymentId,
      'paid_amount'=>$invoicePayment,
      'advance_amount'=>$advanceAmount,
      'remaining_amount'=>$remainingAfter,
      'fully_paid'=>$fullyPaid,
      'message_queued'=>$fullyPaid && !empty($messageResult['queued']),
      'auto_print'=>$fullyPaid && finance_payment_auto_print_enabled($conn),
      'error'=>'',
      'warning'=>$warning,
    ];
  }
}

if (!function_exists('finance_add_subscriber_credit')) {
  function finance_add_subscriber_credit(mysqli $conn, int $subscriberId, float $amount, array $options = []): array {
    $subscriberId = (int)$subscriberId;
    $amount = round($amount, 2);
    if ($subscriberId <= 0 || $amount <= 0) return ['ok'=>false, 'error'=>'بيانات الدفعة المقدمة غير صحيحة'];
    finance_ensure_payments_table($conn);
    $collection = finance_default_collection_context($options);
    $paidAt = (string)($options['paid_at'] ?? date('Y-m-d H:i:s'));
    $method = (string)($options['method'] ?? 'نقدي');
    $note = trim((string)($options['note'] ?? 'دفعة مقدمة للمشترك'));
    $createdBy = (string)($options['created_by'] ?? finance_payment_admin_name());
    $reference = (string)($options['reference'] ?? ('ADV#' . $subscriberId . '#' . date('YmdHis')));
    $key = substr(trim((string)($options['idempotency_key'] ?? ('ADVANCE_PAYMENT:' . $subscriberId . ':' . ua_agent_uuid()))), 0, 191);
    $agentId = isset($options['agent_id']) ? (int)$options['agent_id'] : null;
    try {
      $conn->begin_transaction();
      $subRs = @$conn->query("SELECT id FROM subscribers WHERE id={$subscriberId} AND COALESCE(is_deleted,0)=0 LIMIT 1 FOR UPDATE");
      if (!$subRs || !$subRs->num_rows) throw new RuntimeException('المشترك غير موجود');
      $payment = finance_record_payment_row($conn, 0, $subscriberId, $amount, $method, $note, $createdBy, $paidAt, $reference, $agentId, [
        'idempotency_key'=>$key,
        'source_instance_id'=>$collection['source_instance_id'],
        'source_type'=>$collection['source_type'],
        'source_ref'=>$collection['source_ref'],
        'source_name'=>$collection['source_name'],
        'channel'=>$collection['channel'],
      ]);
      if (empty($payment['ok'])) throw new RuntimeException((string)($payment['error'] ?? 'تعذر تسجيل الدفعة'));
      $paymentId = (int)($payment['id'] ?? 0);
      $credit = billing_credit_post($conn, $subscriberId, 'CREDIT', $amount, [
        'entry_type'=>'ADVANCE_PAYMENT',
        'payment_id'=>$paymentId,
        'source_instance_id'=>$collection['source_instance_id'],
        'idempotency_key'=>'CREDIT:' . $key,
        'note'=>$note,
        'created_by_type'=>$collection['source_type'],
        'created_by_id'=>$agentId,
      ]);
      if (empty($credit['ok'])) throw new RuntimeException((string)($credit['error'] ?? 'تعذر إضافة رصيد المشترك'));
      $allocation = billing_allocate_available_credit($conn, $subscriberId);
      if (empty($allocation['ok'])) throw new RuntimeException('تعذر توزيع الرصيد على الفواتير');
      $conn->commit();
    } catch (Throwable $e) {
      @$conn->rollback();
      return ['ok'=>false, 'error'=>$e->getMessage()];
    }
    $outstanding = billing_subscriber_outstanding($conn, $subscriberId);
    if ($outstanding <= 0.00001) {
      $latest = @$conn->query("SELECT id FROM invoices WHERE subscriber_id={$subscriberId} ORDER BY id DESC LIMIT 1");
      $latestRow = $latest ? $latest->fetch_assoc() : [];
      $latestId = (int)($latestRow['id'] ?? 0);
      if ($latestId > 0) {
        $invoice = finance_fetch_invoice_with_subscriber($conn, $latestId);
        if ($invoice) finance_restore_subscriber_after_invoice_payment($conn, $invoice, $latestId);
      }
    }
    return [
      'ok'=>true,
      'payment_id'=>$paymentId,
      'balance'=>billing_credit_balance($conn, $subscriberId),
      'allocated'=>(float)($allocation['applied'] ?? 0),
      'covered'=>(int)($allocation['covered'] ?? 0),
      'outstanding'=>$outstanding,
    ];
  }
}

if (!function_exists('finance_unpay_invoice')) {
  function finance_unpay_invoice(mysqli $conn, int $invoiceId): array {
    $invoiceId = (int)$invoiceId;
    if ($invoiceId <= 0) return ['ok'=>false,'error'=>'رقم الفاتورة غير صحيح','warning'=>''];

    ua_agent_schema_migrate($conn);
    try {
      $conn->begin_transaction();
      $locked = @$conn->query("SELECT i.*,s.id sid,s.ppp_username FROM invoices i LEFT JOIN subscribers s ON i.subscriber_id=s.id WHERE i.id={$invoiceId} LIMIT 1 FOR UPDATE");
      if (!$locked || !$locked->num_rows) throw new RuntimeException('الفاتورة غير موجودة.');
      $row = $locked->fetch_assoc();
      if (!finance_invoice_is_paid($row)) throw new RuntimeException('هذه الفاتورة ليست مدفوعة.');

      $paymentRows = [];
      $payment = @$conn->query("SELECT * FROM payments WHERE invoice_id={$invoiceId} AND payment_status='POSTED' ORDER BY id DESC FOR UPDATE");
      if ($payment) {
        while ($paymentRow = $payment->fetch_assoc()) $paymentRows[] = $paymentRow;
      }
      foreach ($paymentRows as $paymentRow) {
        $paymentId = (int)($paymentRow['id'] ?? 0);
        if ($paymentId <= 0) continue;
        $collectionReverse = billing_reverse_collection_payment($conn, $paymentId);
        if (empty($collectionReverse['ok'])) throw new RuntimeException((string)($collectionReverse['error'] ?? 'فشل عكس رصيد التحصيل'));
        if ((int)($paymentRow['agent_id'] ?? 0) > 0) {
          $entryRs = @$conn->query("SELECT id FROM agent_ledger_entries WHERE payment_id={$paymentId} AND status='POSTED' ORDER BY id DESC LIMIT 1");
          if ($entryRs && ($entry = $entryRs->fetch_assoc())) {
            $reversed = ua_agent_ledger_reverse($conn, (int)$entry['id'], 'إلغاء تسديد الفاتورة رقم ' . $invoiceId, ['type'=>'admin'], false);
            if (!$reversed['ok']) throw new RuntimeException($reversed['error']);
          }
        }
        if (!@$conn->query("UPDATE payments SET payment_status='REVERSED' WHERE id={$paymentId}")) throw new RuntimeException($conn->error);
      }
      $creditApplied = round((float)($row['credit_applied'] ?? 0), 2);
      if ($creditApplied > 0) {
        $creditReturn = billing_credit_post($conn, (int)($row['sid'] ?? 0), 'CREDIT', $creditApplied, [
          'entry_type'=>'INVOICE_CREDIT_REVERSAL',
          'invoice_id'=>$invoiceId,
          'idempotency_key'=>'CREDIT_REFUND:' . $invoiceId . ':' . date('YmdHis'),
          'note'=>'إعادة الرصيد المقدم بعد إلغاء تسديد الفاتورة رقم ' . $invoiceId,
          'created_by_type'=>'admin',
        ]);
        if (empty($creditReturn['ok'])) throw new RuntimeException((string)($creditReturn['error'] ?? 'فشل إعادة الرصيد المقدم'));
        @ $conn->query("UPDATE invoices SET credit_applied=0.00 WHERE id={$invoiceId} LIMIT 1");
      }
      $upd = finance_set_invoice_unpaid_state($conn, $invoiceId);
      if (!$upd['ok']) throw new RuntimeException($upd['error']);
      $conn->commit();
    } catch (Throwable $e) {
      @$conn->rollback();
      return ['ok'=>false,'error'=>$e->getMessage(),'warning'=>''];
    }

    $sid = (int)($row['sid'] ?? 0);
    if ($sid > 0 && col_exists($conn, 'subscribers', 'status')) {
      @ $conn->query("UPDATE subscribers SET status='متوقف' WHERE id={$sid}");
    }

    $pppUser = trim((string)($row['ppp_username'] ?? ''));
    if ($pppUser !== '') {
      $okMt = mt_ppp_secret_set_disabled($conn, $pppUser, true);
      if (!$okMt) {
        return ['ok'=>true,'error'=>'','warning'=>'تم إلغاء التسديد ✅ لكن فشل تعطيل PPP على MikroTik للمستخدم: '.$pppUser];
      }
    }

    return ['ok'=>true,'error'=>'','warning'=>''];
  }
}
