<?php
/**
 * ops_reassign.php — Pont d'écriture Ops → legacy osTicket (gestionclient).
 * Déployé SUR facturation.targo.ca (hôte de l'app) → la connexion DB hérite des droits d'ÉCRITURE
 * de l'app (aucun GRANT à modifier ; le user du hub @10.100.5.61 reste SELECT-only).
 *
 * Actions (POST, x-www-form-urlencoded ou JSON), header `X-Ops-Token: <token>` OBLIGATOIRE :
 *   action=reassign  ticket_id, staff_id, [assist_ids=CSV] [notify=1] [address] → assign_to + participant(assistants) + followed_by + ticket_msg
 *                    notify=1 → courriel d'assignation au tech (résumé + lien Répondre + Adresse), via PHPMailer6 OAuth2 (ops_secret.php)
 *   action=close     ticket_id                              → status=closed + date_closed + closed_by + réouverture enfants + ticket_msg
 *   action=schedule  ticket_id, due_date=<epoch>, [due_time] → due_date + due_time + ticket_msg (write-back horaire Ops→F)
 *
 * FIDÈLE à ticket_view.php : format participant `-id-;-id-`, followers JSON, log CHANGE LOG, respect du lock_name,
 * dates en epoch (time()), réouverture des tickets enfants `waiting_for` à la fermeture.
 *
 * ⚠️ Secrets (TOKEN + creds DB) : remplis UNIQUEMENT dans la copie serveur, jamais committés. Ici = placeholders.
 */
header('Content-Type: application/json; charset=utf-8');

// ─── secrets : fichier serveur HORS REPO `ops_secret.php` (à côté de ce fichier) qui définit :
//     $OPS_TOKEN, $DB_USER, $DB_PASS  +  creds OAuth Gmail $oauthEmail,$clientId,$clientSecret,$refreshToken
//     (réutilise les mêmes creds que ticket_view.php). JAMAIS committé.
require __DIR__ . '/ops_secret.php';
$DB_HOST   = '10.100.80.100';
$DB_NAME   = 'gestionclient';
$OPS_STAFF = 3301; // auteur des entrées ticket_msg (Tech Targo)

function out($a, $code = 200) { http_response_code($code); echo json_encode($a, JSON_UNESCAPED_UNICODE); exit; }

// ─── auth ───
$tok = $_SERVER['HTTP_X_OPS_TOKEN'] ?? ($_POST['token'] ?? '');
if (!is_string($tok) || !hash_equals($OPS_TOKEN, $tok)) out(['ok' => false, 'error' => 'forbidden'], 403);

$action = $_POST['action'] ?? 'reassign';
// Auteur réel = staff legacy du répartiteur Ops (mappé depuis son email Authentik côté hub) ; repli = Tech Targo.
$actor = (int)($_POST['actor_staff_id'] ?? 0);

// ─── connexion (droits write hérités de l'hôte app) ───
$db = @new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME);
if ($db->connect_errno) out(['ok' => false, 'error' => 'db_connect: ' . $db->connect_error], 500);
$db->set_charset('utf8');
$now = time();
$logStaff = $actor > 0 ? $actor : $OPS_STAFF;

function ops_log($db, $ticket, $staff, $msg) {
  $m = $db->real_escape_string('<hr><p>CHANGE LOG (Ops):</p><p>' . $msg . '</p>');
  $db->query("INSERT INTO ticket_msg (ticket_id, staff_id, msg, date_orig, public) VALUES ($ticket, $staff, '$m', " . time() . ", 0)");
}

// ─── FERMETURE EN LOT : 1 connexion, N tickets (efficace — évite N allers-retours HTTPS + mysqli) ───
if ($action === 'batch_close') {
  $ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string)($_POST['ticket_ids'] ?? ''))))));
  $closed = 0; $skipped = 0; $lockedN = 0; $results = [];
  foreach ($ids as $tid) {
    $r = $db->query("SELECT status, lock_name FROM ticket WHERE id = $tid LIMIT 1"); $row = $r ? $r->fetch_assoc() : null;
    if (!$row || $row['status'] === 'closed') { $skipped++; $results[] = ['t' => $tid, 'skip' => true]; continue; }
    if (trim((string)$row['lock_name']) !== '') { $lockedN++; $results[] = ['t' => $tid, 'locked' => $row['lock_name']]; continue; }
    $db->query("UPDATE ticket SET status='closed', date_closed=$now, closed_by=$logStaff, last_update=$now WHERE id=$tid AND status<>'closed'");
    $db->query("UPDATE ticket SET status='open', last_update=$now WHERE waiting_for=$tid AND status<>'closed'");
    ops_log($db, $tid, $logStaff, 'Ticket fermé via Ops (lot).');
    $closed++; $results[] = ['t' => $tid, 'ok' => true];
  }
  out(['ok' => true, 'action' => 'batch_close', 'closed' => $closed, 'skipped' => $skipped, 'locked' => $lockedN, 'results' => $results]);
}

// ─── POST : ajouter une note/réponse au fil d'un ticket depuis Ops ───
//   public=0 (DÉFAUT) = note INTERNE (jamais envoyée au client) · public=1 = message public (visible dans le ticket).
//   Même mécanisme que ops_log (INSERT ticket_msg) → apparaît dans le fil lu par le hub. (int)-cast + real_escape = sûr.
if ($action === 'post') {
  $ticket = (int)($_POST['ticket_id'] ?? 0);
  $msg = trim((string)($_POST['msg'] ?? ''));
  $public = ((($_POST['public'] ?? '0')) === '1') ? 1 : 0;
  if ($ticket <= 0 || $msg === '') out(['ok' => false, 'error' => 'ticket_id + msg requis'], 400);
  $r = $db->query("SELECT id FROM ticket WHERE id = $ticket LIMIT 1");
  if (!$r || !$r->fetch_assoc()) out(['ok' => false, 'error' => 'ticket introuvable'], 404);
  $m = $db->real_escape_string(mb_substr($msg, 0, 6000));
  $db->query("INSERT INTO ticket_msg (ticket_id, staff_id, msg, date_orig, public) VALUES ($ticket, $logStaff, '$m', $now, $public)");
  $newId = $db->insert_id;
  $db->query("UPDATE ticket SET last_update=$now WHERE id=$ticket");
  out(['ok' => true, 'action' => 'post', 'public' => $public, 'msg_id' => $newId]);
}

// ─── LECTURE / MONITORING (sync F→OPS frais — le miroir local peut être en retard) ───
// Toutes les valeurs numériques sont (int)-castées → interpolation sûre (pas d'injection).
//   action=ping              → fraîcheur + compteurs (max date_last, max ids, heure serveur)
//   action=changes_since     entity=account|delivery|service, since=<epoch date_last>, since_id=<id>, [limit=1000]
//                            → lignes modifiées/nouvelles + max_date_last/max_id (prochain watermark) + has_more (pagination)
if ($action === 'ping') {
  $r = $db->query("SELECT
      (SELECT MAX(date_last) FROM account) AS max_date_last,
      (SELECT COUNT(*) FROM account)       AS accounts,
      (SELECT MAX(id) FROM account)        AS max_account_id,
      (SELECT MAX(id) FROM delivery)       AS max_delivery_id,
      (SELECT MAX(id) FROM service)        AS max_service_id,
      UNIX_TIMESTAMP()                     AS server_time");
  out(['ok' => true, 'action' => 'ping', 'f' => ($r ? $r->fetch_assoc() : null)]);
}

if ($action === 'changes_since') {
  $entity  = $_POST['entity'] ?? 'account';
  $since   = (int)($_POST['since'] ?? 0);      // watermark date_last (epoch)
  $sinceId = (int)($_POST['since_id'] ?? 0);   // watermark id (inserts sans date de modif)
  $limit   = min(5000, max(1, (int)($_POST['limit'] ?? 1000)));
  if ($entity === 'account') {
    // incrémental (since>0) : date_last couvre create+update, 1 page large ; full (since=0) : keyset pur sur id
    $wsql = $since > 0 ? "date_last > $since" : "id > $sinceId";
    $q = $db->query("SELECT id, first_name, last_name, company, group_id, language_id, status, email, tel_home, cell, commercial, mauvais_payeur, stripe_id, ppa, date_orig, date_last
                     FROM account WHERE $wsql ORDER BY id ASC LIMIT $limit");
  } elseif ($entity === 'delivery') {
    $q = $db->query("SELECT id, account_id, name, address1, address2, city, state, zip, longitude, latitude, tel_home, cell, email, date_orig
                     FROM delivery WHERE id > $sinceId ORDER BY id ASC LIMIT $limit");
  } elseif ($entity === 'service') {
    $q = $db->query("SELECT id, delivery_id, product_id, status, payment_recurrence, date_orig, date_suspended, date_next_invoice, date_end_contract, hijack_price, forfait_internet
                     FROM service WHERE id > $sinceId ORDER BY id ASC LIMIT $limit");
  } else {
    out(['ok' => false, 'error' => 'entity inconnue (account|delivery|service)'], 400);
  }
  if (!$q) out(['ok' => false, 'error' => 'query: ' . $db->error], 500);
  $rows = []; $maxTs = $since; $maxId = $sinceId;
  while ($row = $q->fetch_assoc()) {
    $rows[] = $row;
    if (isset($row['date_last'])) $maxTs = max($maxTs, (int)$row['date_last']);
    $maxId = max($maxId, (int)$row['id']);
  }
  out(['ok' => true, 'action' => 'changes_since', 'entity' => $entity, 'count' => count($rows),
       'max_date_last' => $maxTs, 'max_id' => $maxId, 'has_more' => count($rows) >= $limit, 'rows' => $rows]);
}

// Tickets OUVERTS assignés (terrain) dans une fenêtre de dates → timeline OPS (lecture F LIVE, pas le miroir).
if ($action === 'tickets_window') {
  $lo = (int)($_POST['lo'] ?? 0); $hi = (int)($_POST['hi'] ?? 0);
  $staff = preg_replace('/[^0-9,]/', '', (string)($_POST['staff'] ?? '')); // CSV d'ids staff (sanitized = anti-injection)
  if ($staff === '' || $lo <= 0 || $hi <= 0) out(['ok' => false, 'error' => 'lo/hi/staff requis'], 400);
  $q = $db->query("SELECT t.id, t.subject, t.assign_to, t.due_date, dd.name AS dept
                   FROM ticket t LEFT JOIN ticket_dept dd ON dd.id = t.dept_id
                   WHERE t.status='open' AND t.assign_to IN ($staff) AND t.due_date BETWEEN $lo AND $hi");
  if (!$q) out(['ok' => false, 'error' => 'query: ' . $db->error], 500);
  $rows = []; while ($r = $q->fetch_assoc()) $rows[] = $r;
  out(['ok' => true, 'action' => 'tickets_window', 'count' => count($rows), 'rows' => $rows]);
}

// État RÉEL d'une LISTE de tickets dans F (réconciliation OPS : aligner les Dispatch Job sur la vérité F).
// SELECT-only, aucune limite de statut/dept : on veut savoir si chaque ticket est encore ouvert ET à qui il est assigné.
if ($action === 'ticket_status') {
  $ids = preg_replace('/[^0-9,]/', '', (string)($_POST['ids'] ?? '')); // CSV d'ids ticket (sanitized = anti-injection)
  if ($ids === '') out(['ok' => true, 'action' => 'ticket_status', 'count' => 0, 'rows' => []]);
  $q = $db->query("SELECT t.id, t.status, t.assign_to, t.due_date, dd.name AS dept, t.subject
                   FROM ticket t LEFT JOIN ticket_dept dd ON dd.id = t.dept_id
                   WHERE t.id IN ($ids)");
  if (!$q) out(['ok' => false, 'error' => 'query: ' . $db->error], 500);
  $rows = []; while ($r = $q->fetch_assoc()) $rows[] = $r;
  out(['ok' => true, 'action' => 'ticket_status', 'count' => count($rows), 'rows' => $rows]);
}

// ─── actions sur 1 ticket ───
$ticket = (int)($_POST['ticket_id'] ?? 0);
if ($ticket <= 0) out(['ok' => false, 'error' => 'ticket_id requis'], 400);
$res = $db->query("SELECT id, status, assign_to, lock_name, followed_by FROM ticket WHERE id = $ticket LIMIT 1");
$t = $res ? $res->fetch_assoc() : null;
if (!$t) out(['ok' => false, 'error' => 'ticket introuvable'], 404);
if ($t['status'] === 'closed') out(['ok' => false, 'error' => 'ticket déjà fermé', 'status' => 'closed']);
// LOCK : un staff édite le ticket dans le legacy → on NE clobbe PAS (le répartiteur réessaie)
if (trim((string)$t['lock_name']) !== '') out(['ok' => false, 'error' => 'verrouillé', 'locked_by' => $t['lock_name']]);

if ($action === 'close') {
  $db->query("UPDATE ticket SET status='closed', date_closed=$now, closed_by=$logStaff, last_update=$now WHERE id=$ticket AND status<>'closed'");
  $aff = $db->affected_rows;
  // réouvre les tickets enfants en attente de celui-ci (fidèle au legacy)
  $db->query("UPDATE ticket SET status='open', last_update=$now WHERE waiting_for=$ticket AND status<>'closed'");
  ops_log($db, $ticket, $logStaff, 'Ticket fermé via Ops.');
  out(['ok' => true, 'action' => 'close', 'affected' => $aff]);
}

// ─── HORAIRE : write-back de la date/heure planifiées dans Ops → ticket.due_date/due_time ───
//   due_date = epoch (int) OBLIGATOIRE ; due_time = 'HH:MM' | 'am' | 'pm' | 'day' (varchar, validé, sinon vidé).
//   Respecte lock/closed (préambule ci-dessus). N'écrase PAS d'autres champs. Idempotent (affected=0 si déjà à cette valeur).
if ($action === 'schedule') {
  $due = (int)($_POST['due_date'] ?? 0);
  if ($due <= 0) out(['ok' => false, 'error' => 'due_date (epoch) requis'], 400);
  $dt = trim((string)($_POST['due_time'] ?? ''));
  if ($dt !== '' && !preg_match('/^([01]?\d|2[0-3]):[0-5]\d$/', $dt) && !in_array(strtolower($dt), ['am', 'pm', 'day'], true)) $dt = '';
  $dtEsc = $db->real_escape_string($dt);
  $db->query("UPDATE ticket SET due_date=$due, due_time='$dtEsc', last_update=$now WHERE id=$ticket AND status<>'closed'");
  $aff = $db->affected_rows;
  ops_log($db, $ticket, $logStaff, 'Horaire mis à jour via Ops : ' . date('Y-m-d', $due) . ($dt !== '' ? ' ' . $dt : '') . '.');
  out(['ok' => true, 'action' => 'schedule', 'affected' => $aff, 'due_date' => $due, 'due_time' => $dt]);
}

// ─── reassign (+ assistants) ───
$staff = (int)($_POST['staff_id'] ?? 0);
if ($staff <= 0) out(['ok' => false, 'error' => 'staff_id requis'], 400);
$chk = $db->query("SELECT id, first_name, last_name, email FROM staff WHERE id=$staff AND status=1");
$s = $chk ? $chk->fetch_assoc() : null;
if (!$s) out(['ok' => false, 'error' => 'staff inactif ou introuvable', 'staff_id' => $staff]);

// assistants : CSV d'ids → participant `-id-;-id-` + ajout aux followers (followed_by JSON) — FIDÈLE à ticket_view.php
$assistCsv = trim((string)($_POST['assist_ids'] ?? ''));
$assistIds = $assistCsv === '' ? [] : array_values(array_unique(array_filter(array_map('intval', explode(',', $assistCsv)))));
$participant = '';
foreach ($assistIds as $a) { $participant .= ($participant === '' ? '' : ';') . "-$a-"; }
$aFollow = json_decode($t['followed_by'] !== '' && $t['followed_by'] !== null ? $t['followed_by'] : '{}', true);
if (!is_array($aFollow)) $aFollow = [];
foreach ($assistIds as $a) { if (!array_key_exists((string)$a, $aFollow)) $aFollow[(string)$a] = ['child' => 0]; }
$jsonFollow = $db->real_escape_string(json_encode((object)$aFollow));
$participantEsc = $db->real_escape_string($participant);

$db->query("UPDATE ticket SET assign_to=$staff, participant='$participantEsc', followed_by='$jsonFollow', last_update=$now WHERE id=$ticket AND status<>'closed'");
$aff = $db->affected_rows;
$who = trim(($s['first_name'] ?? '') . ' ' . ($s['last_name'] ?? ''));
$log = "Réassigné à #$staff ($who) via Ops." . ($assistIds ? ' Assistants : ' . implode(', ', $assistIds) . '.' : '');
ops_log($db, $ticket, $logStaff, $log);

// ─── avis courriel au nouveau tech (notify=1) — MÊME contenu que ticket_view.php (résumé + lien Répondre),
//     + ligne « Adresse » fournie par Ops. Envoi via PHPMailer6 OAuth2 (creds dans ops_oauth_boot.php, hors repo). ───
$notified = false; $notifyErr = null; $notifyTo = '';
if (!empty($_POST['notify'])) {
  $staffEmail = trim((string)($s['email'] ?? ''));
  $notifyTo = $staffEmail;
  if ($staffEmail === '') { $notifyErr = 'staff sans courriel'; }
  else {
    $opsAddress = trim((string)($_POST['address'] ?? '')); // adresse de service propre (Ops)
    $tq = $db->query("SELECT subject, dept_id, account_id, status, due_date, due_time, bon_id, wizard_fibre FROM ticket WHERE id=$ticket LIMIT 1");
    $tr = $tq ? $tq->fetch_assoc() : [];
    $subject  = (string)($tr['subject'] ?? '');
    $deptId   = (int)($tr['dept_id'] ?? 0);
    $accId    = (int)($tr['account_id'] ?? 0);
    $tstatus  = (string)($tr['status'] ?? '');
    $dueEpoch = (int)($tr['due_date'] ?? 0);
    $dueStr   = $dueEpoch > 0 ? date('d-m-Y', $dueEpoch) : 'sans date';
    $dueTime  = trim((string)($tr['due_time'] ?? ''));
    $dueTimeStr = ($dueTime !== '' && $dueEpoch > 0) ? " - $dueTime" : '';
    $bonId    = trim((string)($tr['bon_id'] ?? ''));
    $wizFibre = trim((string)($tr['wizard_fibre'] ?? ''));
    $dn = $db->query("SELECT name FROM ticket_dept WHERE id=$deptId LIMIT 1"); $dnr = $dn ? $dn->fetch_assoc() : null;
    $deptName = (string)($dnr['name'] ?? '');
    $cn = $db->query("SELECT first_name, last_name, company, customer_id FROM account WHERE id=$accId LIMIT 1"); $cnr = $cn ? $cn->fetch_assoc() : null;
    $clientName = $cnr ? trim(($cnr['first_name'] ?? '') . ' ' . ($cnr['last_name'] ?? '') . ' ' . ($cnr['company'] ?? '') . ' - ' . ($cnr['customer_id'] ?? '')) : '';
    // liens ONU (fibre) / bon de travail — fidèle à ticket_view.php
    $onu = '';
    if ($wizFibre !== '') { $wi = explode('|', $wizFibre); $onu = "<a href='https://store.targo.ca/targo/rep_ticket/connect_fibre.php?tt=$ticket&td=" . ($wi[0] ?? '') . "&ts=$staff&tf=" . ($wi[1] ?? '') . "&tp=" . ($wi[2] ?? '') . "'>Connecter ONU</a><br><br>"; }
    if ($deptId === 26) $onu = "<a href='https://store.targo.ca/targo/rep_ticket/change_fibre.php?tt=$ticket'>Remplacer ONU</a><br><br>";
    $bonLink  = $bonId !== '' ? "Ouvrir <b>Bon de Travail</b>: https://facturation.targo.ca/facturation/accueil.php?menu=bon_travail&bon_id=$bonId <br><br>" : '';
    $addrLine = $opsAddress !== '' ? "<b>Adresse</b>: " . htmlspecialchars($opsAddress, ENT_QUOTES) . "<br/>" : '';
    $replyLink = "https://store.targo.ca/targo/reply_ticket.php?ticket=$ticket&staff=$staff";
    $subjectLine = html_entity_decode("[Ticket #$ticket] $subject [$deptName]", ENT_QUOTES);
    $body = "Ce ticket vous a &eacute;t&eacute; assign&eacute;.<br/><br/>"
      . "R&eacute;pondre: $replyLink<br/><br/>"
      . $onu . $bonLink
      . "<b>Ticket ID</b>: <a href='https://facturation.targo.ca/facturation/accueil.php?menu=ticket_view&id=$ticket'>$ticket</a><br/>"
      . "<b>Client</b>: $clientName<br/><br/>"
      . "<b>Sujet</b>: $subject<br/>"
      . "<b>Status</b>: $tstatus<br/>"
      . "<b>Due date</b>: $dueStr $dueTimeStr<br/>"
      . $addrLine
      . "<b>D&eacute;partement</b>: $deptName<br/><br/>";
    try {
      // L'app legacy (vendor + PHPMailer6) est dans ./facturation quand ce script est au docroot /var/www ; sinon ici.
      $APP_DIR = is_dir(__DIR__ . '/facturation/PHPMailer6') ? __DIR__ . '/facturation' : __DIR__;
      require_once $APP_DIR . '/vendor/autoload.php'; // League\OAuth2 (Google provider) — déjà dans l'app
      require_once $APP_DIR . '/PHPMailer6/Exception.php';
      require_once $APP_DIR . '/PHPMailer6/PHPMailer.php';
      require_once $APP_DIR . '/PHPMailer6/SMTP.php';
      require_once $APP_DIR . '/PHPMailer6/OAuth.php';
      $provider = new \League\OAuth2\Client\Provider\Google(['clientId' => $clientId, 'clientSecret' => $clientSecret]); // creds depuis ops_secret.php
      $mail = new \PHPMailer\PHPMailer\PHPMailer(true);
      $mail->isSMTP();
      $mail->Host = gethostbyname('smtp.gmail.com');
      $mail->SMTPOptions = ['ssl' => ['verify_peer_name' => false]];
      $mail->Port = 587;
      $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
      $mail->SMTPAuth = true;
      $mail->AuthType = 'XOAUTH2';
      $mail->setOAuth(new \PHPMailer\PHPMailer\OAuth(['provider' => $provider, 'clientId' => $clientId, 'clientSecret' => $clientSecret, 'refreshToken' => $refreshToken, 'userName' => $oauthEmail]));
      $mail->setFrom('ticket@targointernet.com', 'Ticket');
      $mail->addAddress($staffEmail);
      $mail->Subject = $subjectLine;
      $mail->CharSet = \PHPMailer\PHPMailer\PHPMailer::CHARSET_UTF8;
      $mail->isHTML(true);
      $mail->Body = $body;
      $mail->send();
      $notified = true;
    } catch (\Throwable $e) { $notifyErr = $e->getMessage(); }
  }
}
out(['ok' => true, 'action' => 'reassign', 'affected' => $aff, 'staff' => $staff, 'assistants' => $assistIds, 'notified' => $notified, 'notify_to' => $notifyTo, 'notify_error' => $notifyErr]);
