<?php

namespace App\Controllers;

use App\Core\Database;
use App\Core\Mailer;
use App\Core\Auth;
use App\Models\ActivityLog;

require_once __DIR__ . '/../Onboarding/Support.php';
require_once __DIR__ . '/../Onboarding/PrintPdf.php';
require_once __DIR__ . '/../Onboarding/PdfFill.php';

// Implements the exact same action set the original Google Apps Script
// Code.gs handleAction_() router did: list, get, addHire, updateHire,
// sendPhase, markStatus, submitPhase, verifyI9, template, storePdfs.
// This is the ONLY backend the unmodified hammer-head-onboarding.html
// file talks to (via its existing save()/apiGet() functions) — nothing
// in that file's JS changed to call this; it just calls APPS_SCRIPT_URL,
// which now points here instead of Google.
class OnboardingApiController
{
    private function respond(array $data): void
    {
        header('Content-Type: application/json');
        echo json_encode($data);
    }

    // Looks up the real project_id/department_id for "Employee Onboarding"
    // so activity log entries link back to the right project, same as any
    // other Workspace activity item.
    private function onbProjectIds(): array
    {
        static $cached = null;
        if ($cached !== null) return $cached;
        $db = Database::connection();
        $row = $db->query("SELECT id, department_id FROM projects WHERE code = 'employee_onboarding' LIMIT 1")->fetch();
        $cached = $row ? [(int)$row['id'], (int)$row['department_id']] : [null, null];
        return $cached;
    }

    // Logs an activity item for the currently logged-in staff user (Rachel,
    // etc.) performing a staff-triggered action. Silently does nothing if
    // there's no logged-in session (e.g. a stale/expired one) — never
    // blocks the actual onboarding action from completing.
    private function logStaffActivity(string $action, string $description): void
    {
        try {
            $user = Auth::currentUser();
            if (!$user) { error_log('logStaffActivity: no current user in session for action=' . $action); return; }
            [$projectId, $deptId] = $this->onbProjectIds();
            ActivityLog::log($user, $action, $description, $deptId, $projectId);
        } catch (\Throwable $e) { error_log('logStaffActivity FAILED for action=' . $action . ': ' . $e->getMessage()); }
    }

    // Logs an activity item for every staff member assigned to Employee
    // Onboarding, attributed to each of them individually — used for
    // participant-triggered events (Phase A/B submitted) where there is no
    // logged-in "actor" to attribute the action to directly.
    private function logActivityForReviewers(string $action, string $description): void
    {
        try {
            $db = Database::connection();
            [$projectId, $deptId] = $this->onbProjectIds();
            $rows = $db->query(
                "SELECT DISTINCT up.user_type, up.user_id, COALESCE(m.name, sa.name, '') AS name
                 FROM user_projects up
                 JOIN projects p ON p.id = up.project_id
                 LEFT JOIN managers m ON up.user_type = 'manager' AND m.id = up.user_id
                 LEFT JOIN superadmins sa ON up.user_type = 'superadmin' AND sa.id = up.user_id
                 WHERE p.code = 'employee_onboarding'"
            )->fetchAll();
            foreach ($rows as $r) {
                ActivityLog::log(
                    ['is_super_admin' => $r['user_type'] === 'superadmin', 'user_type' => $r['user_type'], 'user_id' => $r['user_id'], 'name' => $r['name']],
                    $action, $description, $deptId, $projectId
                );
            }
        } catch (\Throwable $e) { error_log('logActivityForReviewers FAILED for action=' . $action . ': ' . $e->getMessage()); }
    }

    // ---- Owner-aware activity logging (Employee Onboarding only) --------
    // These wrap the two existing helpers above (both left completely
    // unchanged) to additionally route a visibility copy to the hire's
    // packet owner (onboarding_hires.created_by_type/created_by_id) when
    // that owner is a different person than whoever actually performed the
    // action. The actor's own row is always the true, unmodified record of
    // who did it; a second row is only ever added for the owner's
    // visibility, and its description always names the real actor by name
    // — it never claims the owner personally performed the action.

    // For staff-triggered actions where $hire is already known (everything
    // except addHire, where the actor IS the owner by construction, and
    // submitPhase, which has no staff actor at all — see below).
    private function logOnboardingActorAction(array $hire, string $action, string $phrase): void
    {
        $actor = Auth::currentUser();
        if (!$actor) { error_log('logOnboardingActorAction: no current user in session for action=' . $action); return; }
        $actorType = !empty($actor['is_super_admin']) ? 'superadmin' : $actor['user_type'];
        $actorId = (int)$actor['user_id'];

        // 1) The actor's own, unmodified activity entry — identical wording
        //    to what every existing call site already produced.
        $this->logStaffActivity($action, ucfirst($phrase));

        // 2) Owner visibility copy, only if there IS a distinct owner.
        $ownerType = $hire['created_by_type'] ?? null;
        $ownerId = isset($hire['created_by_id']) && $hire['created_by_id'] !== null ? (int)$hire['created_by_id'] : null;
        if ($ownerType === null || $ownerId === null) return; // legacy NULL-owner hire: no second row, nothing to route
        if ($ownerType === $actorType && $ownerId === $actorId) return; // same person: no duplicate

        try {
            [$projectId, $deptId] = $this->onbProjectIds();
            $ownerName = null;
            $db = Database::connection();
            if ($ownerType === 'superadmin') {
                $s = $db->prepare('SELECT full_name FROM superadmins WHERE id=?'); $s->execute([$ownerId]);
                $ownerName = $s->fetchColumn() ?: '';
            } else {
                $s = $db->prepare('SELECT name FROM managers WHERE id=?'); $s->execute([$ownerId]);
                $ownerName = $s->fetchColumn() ?: '';
            }
            $actorName = $actor['name'] ?? 'A staff member';
            ActivityLog::log(
                ['is_super_admin' => $ownerType === 'superadmin', 'user_type' => $ownerType, 'user_id' => $ownerId, 'name' => $ownerName],
                $action, $actorName . ' ' . $phrase . ' — a packet you created.', $deptId, $projectId
            );
        } catch (\Throwable $e) { error_log('logOnboardingActorAction owner-copy FAILED for action=' . $action . ': ' . $e->getMessage()); }
    }

    // For the applicant-triggered Phase A/B submission — there is no staff
    // session to attribute anything to, so this either routes a single row
    // to the hire's owner (clearly worded as the applicant's action), or,
    // for legacy hires with no recorded owner, falls back to the EXACT
    // pre-existing broadcast-to-all-reviewers call/description, so nothing
    // that used to be visible disappears or changes wording.
    private function logOnboardingApplicantAction(array $hire, string $action, string $phrase, string $legacyDescription): void
    {
        $ownerType = $hire['created_by_type'] ?? null;
        $ownerId = isset($hire['created_by_id']) && $hire['created_by_id'] !== null ? (int)$hire['created_by_id'] : null;

        if ($ownerType === null || $ownerId === null) {
            // Legacy fallback: byte-identical to the pre-existing call.
            $this->logActivityForReviewers($action, $legacyDescription);
            return;
        }

        try {
            [$projectId, $deptId] = $this->onbProjectIds();
            $db = Database::connection();
            $ownerName = '';
            if ($ownerType === 'superadmin') {
                $s = $db->prepare('SELECT full_name FROM superadmins WHERE id=?'); $s->execute([$ownerId]);
                $ownerName = $s->fetchColumn() ?: '';
            } else {
                $s = $db->prepare('SELECT name FROM managers WHERE id=?'); $s->execute([$ownerId]);
                $ownerName = $s->fetchColumn() ?: '';
            }
            ActivityLog::log(
                ['is_super_admin' => $ownerType === 'superadmin', 'user_type' => $ownerType, 'user_id' => $ownerId, 'name' => $ownerName],
                $action, 'Applicant ' . $phrase . ' — a packet you sent.', $deptId, $projectId
            );
        } catch (\Throwable $e) { error_log('logOnboardingApplicantAction FAILED for action=' . $action . ': ' . $e->getMessage()); }
    }

    public function handle(): void
    {
        $method = $_SERVER['REQUEST_METHOD'];
        $body = [];
        if ($method === 'POST') {
            $raw = file_get_contents('php://input');
            $body = json_decode($raw, true) ?: [];
        }
        $params = $method === 'POST' ? $body : $_GET;
        $action = $params['action'] ?? '';

        try {
            switch ($action) {
                case 'list':        $this->respond($this->actionList($params)); break;
                case 'deleteHire':  $this->respond($this->actionDeleteHire($params)); break;
                case 'restoreHire': $this->respond($this->actionRestoreHire($params)); break;
                case 'get':          $this->respond($this->actionGet($params)); break;
                case 'addHire':      $this->respond($this->actionAddHire($params)); break;
                case 'updateHire':   $this->respond($this->actionUpdateHire($params)); break;
                case 'sendPhase':    $this->respond($this->actionSendPhase($params)); break;
                case 'markStatus':   $this->respond($this->actionMarkStatus($params)); break;
                case 'submitPhase':  $this->respond($this->actionSubmitPhase($params)); break;
                case 'verifyI9':     $this->respond($this->actionVerifyI9($params)); break;
                case 'template':     $this->respond($this->actionTemplate($params)); break;
                case 'storePdfs':    $this->respond($this->actionStorePdfs($params)); break;
                default:             $this->respond(['ok' => false, 'error' => 'Unknown action: ' . $action]);
            }
        } catch (\Throwable $e) {
            error_log('OnboardingApi error: ' . $e->getMessage());
            $this->respond(['ok' => false, 'error' => $e->getMessage()]);
        }
    }

    // ---- Row <-> JS-object mapping ------------------------------------
    // The frontend (renderHR/renderApplicant/renderI9) expects camelCase
    // keys exactly like the original Google Sheet row shape.
    private function rowToHire(array $r): array
    {
        return [
            'token' => $r['token'], 'eid' => $r['eid'], 'status' => $r['status'],
            'first' => $r['first'], 'middle' => $r['middle'], 'last' => $r['last'],
            'phone' => $r['phone'], 'email' => $r['email'],
            'address' => $r['address'], 'city' => $r['city'], 'state' => $r['state'], 'zip' => $r['zip'],
            'guardCard' => $r['guard_card'], 'guardCardExp' => $r['guard_card_exp'],
            'dl' => $r['dl'], 'dlExp' => $r['dl_exp'], 'firearmsPermit' => $r['firearms_permit'],
            'position' => $r['position'], 'site' => $r['site'],
            'hire' => $r['hire_date'], 'hireType' => $r['hire_type'], 'empStatus' => $r['emp_status'],
            'payRate' => $r['pay_rate'], 'payFreq' => $r['pay_freq'], 'hrRep' => $r['hr_rep'],
            'companyName' => $r['company_name'] ?? '', 'ein' => $r['company_ein'] ?? '', 'stateTaxId' => $r['company_state_tax_id'] ?? '',
            'schedule' => json_decode($r['schedule_json'] ?? '{}', true) ?: [],
            'createdAt' => $r['created_at'], 'updatedAt' => $r['updated_at'],
            // Soft-delete marker (S1/S2). NULL = active, non-null = deleted.
            'deletedAt' => $r['deleted_at'] ?? null,
        ];
    }

    // Public wrapper so other controllers (e.g. the embedded status
    // dashboard) can reuse this exact logic instead of duplicating it.
    // The original private method and all its existing callers are
    // untouched.
    public function attachExtrasPublic(array $hire, int $id): array
    {
        return $this->attachExtras($hire, $id);
    }

    private function attachExtras(array $hire, int $id): array
    {
        $db = Database::connection();

        $fa = $db->prepare("SELECT fields_json, signatures_json FROM onboarding_phase_submissions WHERE hire_id=? AND phase='A'");
        $fa->execute([$id]); $rowA = $fa->fetch();
        if ($rowA) {
            $hire['fields'] = json_decode($rowA['fields_json'], true) ?: [];
            $hire['phaseA_fields'] = $hire['fields'];
            // Regeneration happens after a dashboard reload, so carry the
            // saved per-form signature records forward with the hire.
            $hire['formSigs'] = json_decode($rowA['signatures_json'], true) ?: [];
        }

        $fb = $db->prepare("SELECT fields_json FROM onboarding_phase_submissions WHERE hire_id=? AND phase='B'");
        $fb->execute([$id]); $rowB = $fb->fetch();
        if ($rowB) { $hire['phaseB_fields'] = json_decode($rowB['fields_json'], true) ?: []; }

        $i9 = $db->prepare('SELECT * FROM onboarding_i9_section2 WHERE hire_id=?');
        $i9->execute([$id]); $rowI9 = $i9->fetch();
        if ($rowI9) {
            $hire['i9_section2'] = [
                'doc_path' => $rowI9['doc_path'],
                'listA_title' => $rowI9['list_a_title'], 'listA_auth' => $rowI9['list_a_auth'], 'listA_num' => $rowI9['list_a_num'], 'listA_exp' => $rowI9['list_a_exp'],
                'listB_title' => $rowI9['list_b_title'], 'listB_auth' => $rowI9['list_b_auth'], 'listB_num' => $rowI9['list_b_num'], 'listB_exp' => $rowI9['list_b_exp'],
                'listC_title' => $rowI9['list_c_title'], 'listC_auth' => $rowI9['list_c_auth'], 'listC_num' => $rowI9['list_c_num'], 'listC_exp' => $rowI9['list_c_exp'],
                'additional_info' => $rowI9['additional_info'],
                'employer_business_name' => $rowI9['employer_business_name'], 'employer_rep' => $rowI9['employer_rep'],
                'employer_ein' => $rowI9['employer_ein'], 'ca_payroll_acct' => $rowI9['ca_payroll_acct'],
                'employer_address' => $rowI9['employer_address'], 'first_day' => $rowI9['first_day'],
            ];
        }

        $pdfs = $db->prepare('SELECT title, file_path FROM onboarding_gov_forms WHERE hire_id=?');
        $pdfs->execute([$id]);
        $hire['pdfs'] = array_map(fn($p) => ['title' => $p['title'], 'url' => '/onboarding/download?path=' . urlencode($p['file_path'])], $pdfs->fetchAll());

        // The other 4 onboarding documents (HR Form, Phase A, I-9 summary,
        // Phase B) — separate from the 4 government forms above.
        $docs = $db->prepare('SELECT category, title, file_path FROM onboarding_documents WHERE hire_id=? ORDER BY created_at');
        $docs->execute([$id]);
        $hire['documents'] = array_map(fn($d) => ['category' => $d['category'], 'title' => $d['title'], 'url' => '/onboarding/download?path=' . urlencode($d['file_path'])], $docs->fetchAll());

        return $hire;
    }

    // ---- Actions ---------------------------------------------------------

    // $p['scope'] is optional and additive:
    //   omitted/'active' (default) -> deleted_at IS NULL, i.e. EXACTLY the
    //     same rows this endpoint has always returned (deleted_at is NULL
    //     for every pre-existing row, so default behavior is unchanged).
    //   'deleted' -> deleted_at IS NOT NULL, used only by the new S2
    //     Deleted tab.
    //
    // Ownership visibility (added alongside scope, does not affect it):
    //   Super admin -> no owner filter, sees every record including
    //     historical rows where created_by_type/created_by_id are NULL.
    //   Everyone else -> rows where created_by_type/created_by_id match
    //     their own authenticated session identity, OR rows with NO owner
    //     recorded at all (created_by_type/created_by_id both NULL). This
    //     keeps every pre-existing historical hire visible to every
    //     currently-authorized user exactly as it is today — nothing
    //     disappears — while newly created hires going forward are
    //     visible only to their creator (and super admins).
    // Identity is taken exclusively from Auth::currentUser() (server-side
    // session) — never from $p / the request.
    private function actionList(array $p = []): array
    {
        $db = Database::connection();
        $scope = $p['scope'] ?? 'active';
        $deletedClause = 'deleted_at IS NULL';
        if ($scope === 'deleted') $deletedClause = 'deleted_at IS NOT NULL';
        elseif ($scope === 'all') $deletedClause = '1=1';

        $actor = Auth::currentUser();
        if ($actor && !empty($actor['is_super_admin'])) {
            $stmt = $db->prepare("SELECT * FROM onboarding_hires WHERE $deletedClause ORDER BY updated_at DESC");
            $stmt->execute();
        } else {
            $ownerType = $actor ? $actor['user_type'] : '__none__';
            $ownerId = $actor ? (int)$actor['user_id'] : -1;
            $stmt = $db->prepare("SELECT * FROM onboarding_hires WHERE $deletedClause AND ((created_by_type = ? AND created_by_id = ?) OR (created_by_type IS NULL AND created_by_id IS NULL)) ORDER BY updated_at DESC");
            $stmt->execute([$ownerType, $ownerId]);
        }
        $rows = $stmt->fetchAll();
        $hires = [];
        foreach ($rows as $r) { $h = $this->rowToHire($r); $h = $this->attachExtras($h, (int)$r['id']); $hires[] = $h; }
        return ['ok' => true, 'hires' => $hires];
    }

    // S1 "✕" action: soft-delete only. Never runs SQL DELETE, never
    // touches onboarding_documents/onboarding_gov_forms, never changes
    // the existing `status` value.
    private function actionDeleteHire(array $p): array
    {
        $db = Database::connection();
        $token = $p['token'] ?? '';
        if ($token === '') return ['ok' => false, 'error' => 'Missing token.'];
        $stmt = $db->prepare('SELECT * FROM onboarding_hires WHERE token=?');
        $stmt->execute([$token]);
        $hire = $stmt->fetch();
        if (!$hire) return ['ok' => false, 'error' => 'Hire not found.'];

        $db->prepare('UPDATE onboarding_hires SET deleted_at = NOW() WHERE token = ? AND deleted_at IS NULL')
           ->execute([$token]);

        $this->logOnboardingActorAction($hire, 'onboarding_hire_deleted', 'removed ' . onb_full_name($hire) . ' from active New Hires (moved to Deleted)');
        return ['ok' => true];
    }

    // S2 "Restore" action — the exact mirror of actionDeleteHire above.
    // Only ever sets deleted_at back to NULL on the same existing row.
    // Never runs SQL DELETE or INSERT, never touches status,
    // onboarding_phase_submissions, onboarding_i9_section2,
    // onboarding_documents, or onboarding_gov_forms.
    private function actionRestoreHire(array $p): array
    {
        $db = Database::connection();
        $token = $p['token'] ?? '';
        if ($token === '') return ['ok' => false, 'error' => 'Missing token.'];
        $stmt = $db->prepare('SELECT * FROM onboarding_hires WHERE token=?');
        $stmt->execute([$token]);
        $hire = $stmt->fetch();
        if (!$hire) return ['ok' => false, 'error' => 'Hire not found.'];

        $db->prepare('UPDATE onboarding_hires SET deleted_at = NULL WHERE token = ? AND deleted_at IS NOT NULL')
           ->execute([$token]);

        $this->logOnboardingActorAction($hire, 'onboarding_hire_restored', 'restored ' . onb_full_name($hire) . ' to active New Hires');
        return ['ok' => true];
    }

    private function actionGet(array $p): array
    {
        $db = Database::connection();
        $stmt = $db->prepare('SELECT * FROM onboarding_hires WHERE token=?');
        $stmt->execute([$p['token'] ?? '']);
        $row = $stmt->fetch();
        if (!$row) return ['ok' => true, 'hire' => null];
        $hire = $this->attachExtras($this->rowToHire($row), (int)$row['id']);
        return ['ok' => true, 'hire' => $hire];
    }

    private function actionAddHire(array $p): array
    {
        $db = Database::connection();
        $token = $p['token'] ?: onb_new_token();
        // Ownership: captured exclusively from the server-side authenticated
        // session — never from $p / the frontend payload.
        $actor = Auth::currentUser();
        $createdByType = $actor ? ((!empty($actor['is_super_admin'])) ? 'superadmin' : $actor['user_type']) : null;
        $createdById = $actor ? (int)$actor['user_id'] : null;
        $stmt = $db->prepare(
            "INSERT INTO onboarding_hires
             (token, eid, status, first, middle, last, phone, email, address, city, state, zip,
              guard_card, guard_card_exp, dl, dl_exp, firearms_permit, position, site,
              hire_date, hire_type, emp_status, pay_rate, pay_freq, hr_rep, company_name, company_ein, company_state_tax_id, schedule_json,
              created_by_type, created_by_id)
             VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
             ON DUPLICATE KEY UPDATE email = email" // token unique already prevents dup; kept defensive
        );
        $stmt->execute([
            $token, $p['eid'] ?? '', $p['status'] ?? 'New',
            $p['first'] ?? '', $p['middle'] ?? '', $p['last'] ?? '', $p['phone'] ?? '', $p['email'] ?? '',
            $p['address'] ?? '', $p['city'] ?? '', $p['state'] ?? '', $p['zip'] ?? '',
            $p['guardCard'] ?? '', $p['guardCardExp'] ?: null, $p['dl'] ?? '', $p['dlExp'] ?: null,
            $p['firearmsPermit'] ?? 'No', $p['position'] ?? '', $p['site'] ?? '',
            $p['hire'] ?: null, $p['hireType'] ?? 'New Hire', $p['empStatus'] ?? '',
            $p['payRate'] ?? '', $p['payFreq'] ?? '', $p['hrRep'] ?? '',
            $p['companyName'] ?? '', $p['ein'] ?? '', $p['stateTaxId'] ?? '',
            json_encode($p['schedule'] ?? []),
            $createdByType, $createdById,
        ]);
        $this->logStaffActivity('onboarding_hire_created', 'Added new hire: ' . trim(($p['first'] ?? '') . ' ' . ($p['last'] ?? '')));
        return ['ok' => true, 'token' => $token];
    }

    private function actionUpdateHire(array $p): array
    {
        $db = Database::connection();
        // Read-only lookup added purely to get created_by_type/created_by_id
        // for owner-aware activity logging below — the UPDATE statement and
        // its columns are completely unchanged.
        $lookup = $db->prepare('SELECT * FROM onboarding_hires WHERE token=?');
        $lookup->execute([$p['token'] ?? '']);
        $existingHire = $lookup->fetch();
        $stmt = $db->prepare(
            "UPDATE onboarding_hires SET
             eid=?, first=?, middle=?, last=?, phone=?, email=?, address=?, city=?, state=?, zip=?,
             guard_card=?, guard_card_exp=?, dl=?, dl_exp=?, firearms_permit=?, position=?, site=?,
             hire_date=?, hire_type=?, emp_status=?, pay_rate=?, pay_freq=?, hr_rep=?, company_name=?, company_ein=?, company_state_tax_id=?, schedule_json=?
             WHERE token=?"
        );
        $stmt->execute([
            $p['eid'] ?? '', $p['first'] ?? '', $p['middle'] ?? '', $p['last'] ?? '', $p['phone'] ?? '', $p['email'] ?? '',
            $p['address'] ?? '', $p['city'] ?? '', $p['state'] ?? '', $p['zip'] ?? '',
            $p['guardCard'] ?? '', $p['guardCardExp'] ?: null, $p['dl'] ?? '', $p['dlExp'] ?: null,
            $p['firearmsPermit'] ?? 'No', $p['position'] ?? '', $p['site'] ?? '',
            $p['hire'] ?: null, $p['hireType'] ?? '', $p['empStatus'] ?? '',
            $p['payRate'] ?? '', $p['payFreq'] ?? '', $p['hrRep'] ?? '',
            $p['companyName'] ?? '', $p['ein'] ?? '', $p['stateTaxId'] ?? '',
            json_encode($p['schedule'] ?? []),
            $p['token'] ?? '',
        ]);
        if ($existingHire) {
            $this->logOnboardingActorAction($existingHire, 'onboarding_hire_updated', 'updated hire details for ' . trim(($p['first'] ?? '') . ' ' . ($p['last'] ?? '')));
        }
        return ['ok' => true];
    }

    private function actionSendPhase(array $p): array
    {
        $db = Database::connection();
        $email = trim($p['email'] ?? '');
        if (!$email || !preg_match('/^[^\s@]+@[^\s@]+\.[^\s@]+$/', $email)) {
            return ['ok' => false, 'error' => 'A valid applicant email address is required.'];
        }
        $stmt = $db->prepare('SELECT * FROM onboarding_hires WHERE token=?');
        $stmt->execute([$p['token'] ?? '']);
        $hire = $stmt->fetch();
        if (!$hire) return ['ok' => false, 'error' => 'Hire not found.'];

        $phase = ($p['phase'] ?? 'A') === 'B' ? 'B' : 'A';
        $link = onb_link($hire['token'], $phase);
        $subject = $phase === 'A' ? 'Hammer Head — Welcome! Please complete your onboarding forms' : 'Hammer Head — Final onboarding paperwork';
        $body = "<p>Hi {$hire['first']},</p><p>Please complete your onboarding paperwork using the secure link below:</p><p><a href=\"{$link}\">{$link}</a></p><p>This link is unique to you — no login required.</p><p>Thanks,<br>Hammer Head Security HR</p>";
        $ok = Mailer::send($email, onb_full_name($hire), $subject, $body);
        $mailError = $ok ? null : Mailer::lastError();

        $db->prepare('INSERT INTO onboarding_email_log (hire_id, to_email, subject, body, status, error) VALUES (?,?,?,?,?,?)')
           ->execute([$hire['id'], $email, $subject, $body, $ok ? 'sent' : 'failed', $mailError]);

        if (!$ok) return ['ok' => false, 'error' => 'Email failed to send' . ($mailError ? (': ' . $mailError) : '.')];

        $newStatus = $phase === 'A' ? 'A_sent' : 'B_sent';
        $db->prepare('UPDATE onboarding_hires SET status=? WHERE token=?')->execute([$newStatus, $hire['token']]);

        // Save the HR Form snapshot as a document the moment Phase A is sent
        // (mirrors the original sendPhase_ behavior in Code.gs).
        if ($phase === 'A') {
            try {
                $folder = onb_employee_folder($hire);
                $outPath = $folder . '/HR Form/hr_form.pdf';
                if (!onb_render_hr_form_pdf($hire, $outPath)) {
                    $fields = [];
                    foreach ($hire as $k => $v) { if (!is_array($v)) $fields[$k] = $v; }
                    generate_simple_pdf_local('HR Information Form', $fields, null, $outPath);
                }
                register_onb_document($hire['id'], 'hr_form', 'HR Information Form', $outPath);
            } catch (\Throwable $e) {
                error_log('HR Form PDF generation failed for hire ' . $hire['id'] . ': ' . $e->getMessage());
            }
        }

        $this->logOnboardingActorAction($hire, 'onboarding_phase_sent', "sent Phase $phase to " . onb_full_name($hire));
        return ['ok' => true, 'link' => $link];
    }

    private function actionMarkStatus(array $p): array
    {
        $db = Database::connection();
        $token = $p['token'] ?? '';
        $newStatus = $p['status'] ?? '';
        $stmt = $db->prepare('SELECT * FROM onboarding_hires WHERE token=?');
        $stmt->execute([$token]);
        $hire = $stmt->fetch();

        $db->prepare('UPDATE onboarding_hires SET status=? WHERE token=?')->execute([$newStatus, $token]);

        if ($hire) {
            $this->logOnboardingActorAction($hire, 'onboarding_status_marked', 'marked status "' . $newStatus . '" for ' . onb_full_name($hire));
        }
        return ['ok' => true];
    }

    private function actionSubmitPhase(array $p): array
    {
        $db = Database::connection();
        $stmt = $db->prepare('SELECT * FROM onboarding_hires WHERE token=?');
        $stmt->execute([$p['token'] ?? '']);
        $hire = $stmt->fetch();
        if (!$hire) return ['ok' => false, 'error' => 'Hire not found.'];

        $phase = ($p['phase'] ?? 'A') === 'B' ? 'B' : 'A';
        $fields = $p['fields'] ?? [];
        $forms = $p['forms'] ?? []; // ordered per-form breakdown (title + fields + signature), sent by the wizard
        $signatures = $p['signatures'] ?? [];
        $signerName = $p['signerName'] ?? onb_full_name($hire);

        $fj = json_encode($fields); $sj = json_encode($signatures);
        $ins = $db->prepare(
            "INSERT INTO onboarding_phase_submissions (hire_id, phase, fields_json, signatures_json, signer_name)
             VALUES (?,?,?,?,?)
             ON DUPLICATE KEY UPDATE fields_json=?, signatures_json=?, signer_name=?, submitted_at=NOW()"
        );
        $ins->execute([$hire['id'], $phase, $fj, $sj, $signerName, $fj, $sj, $signerName]);

        $newStatus = $phase === 'A' ? 'A_done' : 'Final';
        $db->prepare('UPDATE onboarding_hires SET status=? WHERE token=?')->execute([$newStatus, $hire['token']]);

        // Save every signed form as its own section of the Phase PDF (all 21
        // forms for Phase A / all 7 for Phase B), in wizard order, each with
        // its own header and signature line — instead of one flat dump of
        // merged fields. Falls back to the old flat dump only if an older
        // client sends no per-form breakdown.
        //
        // This whole block is best-effort: the submission itself was already
        // saved above (fields_json + status), so a PDF-generation problem
        // must never cause the API to report the submission as failed.
        try {
            $folder = onb_employee_folder($hire);
            $sub = $phase === 'A' ? 'Phase A' : 'Phase B';
            $outPath = $folder . "/$sub/{$sub}_signed.pdf";
            if ($forms) {
                if (!onb_render_wizard_pdf("Phase $phase — Signed Forms", $forms, $outPath)) {
                    generate_multiform_pdf_local("Phase $phase — Signed Forms (" . count($forms) . ")", $forms, $outPath);
                }
            } else {
                generate_simple_pdf_local("Phase $phase — Signed Forms", $fields, null, $outPath);
            }
            register_onb_document($hire['id'], $phase === 'A' ? 'phase_a' : 'phase_b', "Phase $phase — Signed Forms", $outPath);
        } catch (\Throwable $e) {
            error_log('Phase ' . $phase . ' PDF generation failed for hire ' . $hire['id'] . ': ' . $e->getMessage());
        }

        if ($phase === 'A') {
            onb_notify_reviewers('Phase A submitted', onb_full_name($hire) . ' submitted Phase A — I-9 Section 2 is ready.', '/onboarding?token=' . $hire['token']);
            $this->logOnboardingApplicantAction($hire, 'onboarding_phase_a_submitted', 'submitted Phase A for ' . onb_full_name($hire), onb_full_name($hire) . ' submitted Phase A');
        } else {
            onb_notify_reviewers('Phase B submitted', onb_full_name($hire) . ' submitted Phase B — onboarding package is complete.', '/onboarding?token=' . $hire['token']);
            $this->logOnboardingApplicantAction($hire, 'onboarding_phase_b_submitted', 'submitted Phase B for ' . onb_full_name($hire) . ' — onboarding complete', onb_full_name($hire) . ' submitted Phase B — onboarding complete');
        }

        return ['ok' => true];
    }

    private function actionVerifyI9(array $p): array
    {
        $db = Database::connection();
        $stmt = $db->prepare('SELECT * FROM onboarding_hires WHERE token=?');
        $stmt->execute([$p['token'] ?? '']);
        $hire = $stmt->fetch();
        if (!$hire) return ['ok' => false, 'error' => 'Hire not found.'];

        $s2 = $p['section2'] ?? [];
        $ins = $db->prepare(
            "INSERT INTO onboarding_i9_section2
             (hire_id, doc_path, list_a_title, list_a_auth, list_a_num, list_a_exp,
              list_b_title, list_b_auth, list_b_num, list_b_exp,
              list_c_title, list_c_auth, list_c_num, list_c_exp, additional_info,
              employer_business_name, employer_rep, employer_ein, ca_payroll_acct,
              employer_address, first_day)
             VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
             ON DUPLICATE KEY UPDATE
              doc_path=VALUES(doc_path),
              list_a_title=VALUES(list_a_title), list_a_auth=VALUES(list_a_auth), list_a_num=VALUES(list_a_num), list_a_exp=VALUES(list_a_exp),
              list_b_title=VALUES(list_b_title), list_b_auth=VALUES(list_b_auth), list_b_num=VALUES(list_b_num), list_b_exp=VALUES(list_b_exp),
              list_c_title=VALUES(list_c_title), list_c_auth=VALUES(list_c_auth), list_c_num=VALUES(list_c_num), list_c_exp=VALUES(list_c_exp),
              additional_info=VALUES(additional_info),
              employer_business_name=VALUES(employer_business_name), employer_rep=VALUES(employer_rep),
              employer_ein=VALUES(employer_ein), ca_payroll_acct=VALUES(ca_payroll_acct),
              employer_address=VALUES(employer_address), first_day=VALUES(first_day),
              verified_at=NOW()"
        );
        $ins->execute([
            $hire['id'], $s2['doc_path'] ?? 'BC',
            $s2['listA_title'] ?? '', $s2['listA_auth'] ?? '', $s2['listA_num'] ?? '', $s2['listA_exp'] ?: null,
            $s2['listB_title'] ?? '', $s2['listB_auth'] ?? '', $s2['listB_num'] ?? '', $s2['listB_exp'] ?: null,
            $s2['listC_title'] ?? '', $s2['listC_auth'] ?? '', $s2['listC_num'] ?? '', $s2['listC_exp'] ?: null,
            $s2['additional_info'] ?? '',
            $s2['employer_business_name'] ?? '', $s2['employer_rep'] ?? '', $s2['employer_ein'] ?? '', $s2['ca_payroll_acct'] ?? '',
            $p['employer_address'] ?? '', $p['first_day'] ?: null,
        ]);
        // Status: only advance on genuine first-time completion (existing
        // hire status was exactly 'A_done', the same condition the existing
        // "Do I-9 §2" button already requires). Editing an already-saved I-9
        // later (status is already I9_done/Training/anything else) leaves
        // the hire's status column completely untouched — only the I-9
        // data itself is updated above.
        if ($hire['status'] === 'A_done') {
            $db->prepare('UPDATE onboarding_hires SET status=? WHERE token=?')->execute(['I9_done', $hire['token']]);
        }

        try {
            $folder = onb_employee_folder($hire);
            $outPath = $folder . '/I-9/i9_section2_summary.pdf';
            $phaseARow = $db->prepare('SELECT fields_json FROM onboarding_phase_submissions WHERE hire_id=? AND phase=?');
            $phaseARow->execute([$hire['id'], 'A']);
            $phaseAFields = json_decode($phaseARow->fetchColumn() ?: '{}', true) ?: [];
            $signerName = $s2['employer_rep'] ?: onb_full_name($hire);
            if (!onb_render_i9_pdf($hire, $phaseAFields, $s2, $signerName, $outPath)) {
                generate_simple_pdf_local('Form I-9 Section 2 Summary', $s2, null, $outPath);
            }
            register_onb_document($hire['id'], 'i9', 'Form I-9 Section 2 Summary', $outPath);
        } catch (\Throwable $e) {
            error_log('I-9 PDF generation failed for hire ' . $hire['id'] . ': ' . $e->getMessage());
        }
        $this->logOnboardingActorAction($hire, 'onboarding_i9_verified', 'completed I-9 Section 2 for ' . onb_full_name($hire));

        return ['ok' => true];
    }

    // Serves the real government PDF template bytes for client-side pdf-lib
    // filling — same shape as the old Code.gs getPdfTemplate_().
    private function actionTemplate(array $p): array
    {
        $cfg = require __DIR__ . '/../../config/config.php';
        $map = ['wotc' => '8850_WOTC_Form_2.pdf', 'de4' => 'de4.pdf', 'w4' => 'fw4.pdf', 'i9' => 'i-9.pdf'];
        $key = $p['template'] ?? '';
        if (!isset($map[$key])) return ['ok' => false, 'error' => 'Unknown template.'];
        $path = rtrim($cfg['storage_path'] ?? '', '/') . '/templates/' . $map[$key];
        if (!is_file($path)) return ['ok' => false, 'error' => 'Template not found: ' . $map[$key]];
        return ['ok' => true, 'name' => $map[$key], 'base64' => base64_encode(file_get_contents($path))];
    }

    // Receives the 4 client-side pdf-lib-filled government PDFs (base64)
    // and stores them in the applicant's folder — same as storePdfs_ in
    // the original Code.gs.
    private function actionStorePdfs(array $p): array
    {
        $db = Database::connection();
        $stmt = $db->prepare('SELECT * FROM onboarding_hires WHERE token=?');
        $stmt->execute([$p['token'] ?? '']);
        $hire = $stmt->fetch();
        if (!$hire) return ['ok' => false, 'error' => 'Hire not found.'];

        $cfg = require __DIR__ . '/../../config/config.php';
        $folder = onb_employee_folder($hire);
        $keyDirs = ['Form W-4' => 'W-4', 'Form DE 4' => 'DE4', 'Form I-9' => 'I-9', 'Form 8850 (WOTC)' => 'WOTC'];
        $created = [];
        foreach ($p['pdfs'] ?? [] as $pdf) {
            $title = $pdf['title'] ?? 'Form';
            $dir = $keyDirs[$title] ?? 'PDFs';
            $safe = preg_replace('/[\\\\\/:*?"<>|]/', '-', $title);
            $outPath = "$folder/$dir/$safe.pdf";
            if (!is_dir(dirname($outPath))) @mkdir(dirname($outPath), 0755, true);
            file_put_contents($outPath, base64_decode($pdf['base64'] ?? ''));
            $rel = str_replace(rtrim($cfg['storage_path'] ?? '', '/') . '/', '', $outPath);
            $formKey = array_search($dir, ['W-4' => 'w4', 'DE4' => 'de4', 'I-9' => 'i9', 'WOTC' => 'wotc']) ?: strtolower($dir);
            $db->prepare(
                "INSERT INTO onboarding_gov_forms (hire_id, form_key, title, file_path) VALUES (?,?,?,?)
                 ON DUPLICATE KEY UPDATE file_path=VALUES(file_path), generated_at=NOW()"
            )->execute([$hire['id'], $formKey, $title, $rel]);
            $created[] = ['title' => $title, 'url' => '/onboarding/download?path=' . urlencode($rel)];
        }
        if ($created) {
            $titles = implode(', ', array_column($created, 'title'));
            $this->logOnboardingActorAction($hire, 'onboarding_gov_forms_stored', 'saved government forms (' . $titles . ') for ' . onb_full_name($hire));
        }
        return ['ok' => true, 'pdfs' => $created];
    }
}

function register_onb_document(int $hireId, string $category, string $title, string $absPath): void
{
    $cfg = require __DIR__ . '/../../config/config.php';
    $rel = str_replace(rtrim($cfg['storage_path'] ?? '', '/') . '/', '', $absPath);
    $db = Database::connection();
    // Replace any previous document of this same category for this hire
    // (e.g. re-sending Phase A regenerates the HR Form snapshot) instead of
    // stacking up duplicate rows every time an invite is resent.
    $db->prepare('DELETE FROM onboarding_documents WHERE hire_id = ? AND category = ?')
       ->execute([$hireId, $category]);
    $db->prepare('INSERT INTO onboarding_documents (hire_id, category, title, file_path) VALUES (?,?,?,?)')
       ->execute([$hireId, $category, $title, $rel]);
}
