<?php
/**
 * Callouts — Slice 8S-3 · Reactive Service
 *
 * Lifecycle:
 *   OPEN (intake) → DISPATCHED (tech assigned) → ON_SITE (arrived)
 *   → RESOLVED (fixed) → CLIENT_ACCEPTED (signature) → CLOSED
 *
 * SLA breach math on resolution: computes hours-beyond-target for response + restore.
 */
class Callouts extends Model
{
    public function nextCalloutNo(): string
    {
        $db  = new DB();
        $yr  = date('Y');
        $r   = $db->mySelect("SELECT callout_no FROM callouts WHERE callout_no LIKE :p ORDER BY c0d DESC LIMIT 1", [':p' => 'CO-' . $yr . '-%']);
        $next = 1;
        if (!empty($r) && preg_match('/CO-\d{4}-(\d+)/', (string)$r[0]['callout_no'], $m)) $next = ((int)$m[1]) + 1;
        return sprintf('CO-%s-%05d', $yr, $next);
    }

    public function list(string $status = '', ?int $contractId = null): array
    {
        $db = new DB();
        $where = "WHERE 1=1";
        $params = [];
        if ($status !== '')       { $where .= " AND co.status = :s"; $params[':s'] = $status; }
        if ($contractId !== null) { $where .= " AND co.contract_id = :c"; $params[':c'] = $contractId; }
        return $db->mySelect(
            "SELECT co.c0d, co.callout_no, co.reported_at, co.status, co.description, co.is_emergency,
                    co.dispatched_at, co.arrived_at, co.resolved_at, co.dispatched_to_name,
                    co.sla_response_breached_hrs, co.sla_restore_breached_hrs,
                    s.name AS site_name, a.asset_no,
                    cat.code AS cat_code, cat.name AS cat_name, cat.color_hex, cat.sla_response_hrs, cat.sla_restoration_hrs,
                    c.contract_no, c.customer_name
               FROM callouts co
          LEFT JOIN service_contract_sites s   ON s.c0d = co.site_id
          LEFT JOIN service_contract_assets a  ON a.c0d = co.asset_id
          LEFT JOIN callout_categories cat     ON cat.c0d = co.category_id
          LEFT JOIN service_contracts c        ON c.c0d = co.contract_id
               $where
              ORDER BY co.reported_at DESC",
            $params
        ) ?: [];
    }

    /** Callouts at one site — used by /Projects/site/{id}. Slice 8Z-z-E. */
    public function listForSite(int $siteId): array
    {
        return (new DB())->mySelect(
            "SELECT co.c0d, co.callout_no, co.reported_at, co.status, co.description, co.is_emergency,
                    co.dispatched_at, co.arrived_at, co.resolved_at, co.dispatched_to_name,
                    co.sla_response_breached_hrs, co.sla_restore_breached_hrs,
                    co.contract_id, co.site_id, co.asset_id,
                    a.asset_no, a.serial_no,
                    cat.code AS cat_code, cat.name AS cat_name, cat.color_hex, cat.sla_response_hrs, cat.sla_restoration_hrs
               FROM callouts co
          LEFT JOIN service_contract_assets a  ON a.c0d = co.asset_id
          LEFT JOIN callout_categories cat     ON cat.c0d = co.category_id
              WHERE co.site_id = :s
              ORDER BY co.reported_at DESC, co.c0d DESC
              LIMIT 200",
            [':s' => $siteId]
        ) ?: [];
    }

    public function find(int $id): array
    {
        $db = new DB();
        $r  = $db->mySelect(
            "SELECT co.*, s.name AS site_name, s.site_code,
                    a.asset_no, a.serial_no, a.make, a.model,
                    cat.code AS cat_code, cat.name AS cat_name, cat.color_hex,
                    cat.sla_response_hrs AS cat_response_hrs, cat.sla_restoration_hrs AS cat_restoration_hrs,
                    c.contract_no, c.customer_name
               FROM callouts co
          LEFT JOIN service_contract_sites s   ON s.c0d = co.site_id
          LEFT JOIN service_contract_assets a  ON a.c0d = co.asset_id
          LEFT JOIN callout_categories cat     ON cat.c0d = co.category_id
          LEFT JOIN service_contracts c        ON c.c0d = co.contract_id
              WHERE co.c0d = :id",
            [':id' => $id]
        );
        return $r[0] ?? [];
    }

    public function partsUsedFor(int $coId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT pu.*, p.sku, p.name AS part_name, p.uom
               FROM callout_parts_used pu
          LEFT JOIN spare_parts p ON p.c0d = pu.part_id
              WHERE pu.callout_id = :c
              ORDER BY pu.c0d",
            [':c' => $coId]
        ) ?: [];
    }

    public function saveIntake(array $d, int $uid, string $uname): int
    {
        $cid  = (int)($d['contract_id'] ?? 0);
        $cat  = (int)($d['category_id'] ?? 0);
        if ($cid <= 0 || $cat <= 0) throw new InvalidArgumentException('contract_id + category_id required.');
        $db = new DB();
        $id = (int)($d['c0d'] ?? 0);
        $data = [
            ':c'   => $cid, ':cat' => $cat,
            ':s'   => !empty($d['site_id']) ? (int)$d['site_id'] : null,
            ':a'   => !empty($d['asset_id']) ? (int)$d['asset_id'] : null,
            ':rn'  => trim((string)($d['reported_by_name'] ?? '')) ?: null,
            ':rp'  => trim((string)($d['reported_by_phone'] ?? '')) ?: null,
            ':ra'  => trim((string)($d['reported_at'] ?? '')) ?: date('Y-m-d H:i:s'),
            ':de'  => trim((string)($d['description'] ?? '')) ?: null,
            ':em'  => !empty($d['is_emergency']) ? 1 : 0,
        ];
        if ($id > 0) {
            $data[':id'] = $id;
            $db->insertUpdate(
                "UPDATE callouts SET
                    contract_id=:c, category_id=:cat, site_id=:s, asset_id=:a,
                    reported_by_name=:rn, reported_by_phone=:rp, reported_at=:ra,
                    description=:de, is_emergency=:em, edited_on=NOW()
                  WHERE c0d=:id",
                $data
            );
            return $id;
        }
        $data[':no']  = $this->nextCalloutNo();
        $data[':u']   = $uid;
        $data[':un']  = $uname;
        $fb = $db->insertUpdate(
            "INSERT INTO callouts
                (callout_no, contract_id, category_id, site_id, asset_id, reported_by_name, reported_by_phone,
                 reported_at, description, is_emergency, status, created_by, created_by_name)
             VALUES (:no, :c, :cat, :s, :a, :rn, :rp, :ra, :de, :em, 'OPEN', :u, :un)",
            $data
        );
        return (int)($fb['last_id'] ?? 0);
    }

    public function dispatch(int $coId, int $techUserId, string $techName): void
    {
        $co = $this->find($coId);
        if (empty($co)) throw new RuntimeException('Callout not found.');
        if ($co['status'] !== 'OPEN') throw new RuntimeException('Only OPEN callouts can be dispatched (status ' . $co['status'] . ').');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE callouts SET status='DISPATCHED', dispatched_at=NOW(), dispatched_to=:u, dispatched_to_name=:un, edited_on=NOW()
              WHERE c0d=:id",
            [':u'=>$techUserId, ':un'=>trim($techName), ':id'=>$coId]
        );
    }

    public function arrive(int $coId): void
    {
        $co = $this->find($coId);
        if (empty($co)) throw new RuntimeException('Callout not found.');
        if ($co['status'] !== 'DISPATCHED') throw new RuntimeException('Only DISPATCHED callouts can be marked ON_SITE (status ' . $co['status'] . ').');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE callouts SET status='ON_SITE', arrived_at=NOW(), edited_on=NOW() WHERE c0d=:id",
            [':id'=>$coId]
        );
    }

    public function resolve(int $coId, array $d): void
    {
        $co = $this->find($coId);
        if (empty($co)) throw new RuntimeException('Callout not found.');
        if (!in_array($co['status'], ['ON_SITE','DISPATCHED'], true)) throw new RuntimeException('Cannot resolve — status ' . $co['status'] . '.');
        // Compute SLA breach: response = dispatched_at or arrived_at - reported_at (whichever first happens);
        //                   restore  = resolved_at - reported_at
        $reportedTs = strtotime((string)$co['reported_at']);
        $respTs     = $co['dispatched_at'] ? strtotime((string)$co['dispatched_at']) : (int)($co['arrived_at'] ? strtotime((string)$co['arrived_at']) : time());
        $respHrs    = max(0.0, round(($respTs - $reportedTs) / 3600, 2));
        $restoreHrs = max(0.0, round((time() - $reportedTs) / 3600, 2));
        $respBreach = max(0.0, round($respHrs   - (float)$co['cat_response_hrs'], 2));
        $restBreach = max(0.0, round($restoreHrs - (float)$co['cat_restoration_hrs'], 2));

        $db = new DB();
        $db->insertUpdate(
            "UPDATE callouts SET
                status='RESOLVED', resolved_at=NOW(),
                resolution_template_id=:rt, resolution_description=:rd,
                investigation_notes = COALESCE(:inv, investigation_notes),
                root_cause          = COALESCE(:rca, root_cause),
                snags               = COALESCE(:sn,  snags),
                labour_hrs=:lh, mileage_km=:mk,
                sla_response_breached_hrs=:rb, sla_restore_breached_hrs=:rsb,
                edited_on=NOW()
              WHERE c0d=:id",
            [':rt'=>!empty($d['resolution_template_id']) ? (int)$d['resolution_template_id'] : null,
             ':rd'=>trim((string)($d['resolution_description'] ?? '')) ?: null,
             ':inv'=>trim((string)($d['investigation_notes'] ?? '')) ?: null,
             ':rca'=>trim((string)($d['root_cause']          ?? '')) ?: null,
             ':sn' =>trim((string)($d['snags']               ?? '')) ?: null,
             ':lh'=>isset($d['labour_hrs']) && $d['labour_hrs'] !== '' ? (float)$d['labour_hrs'] : null,
             ':mk'=>isset($d['mileage_km']) && $d['mileage_km'] !== '' ? (float)$d['mileage_km'] : null,
             ':rb'=>$respBreach, ':rsb'=>$restBreach, ':id'=>$coId]
        );
    }

    /**
     * Slice 8Z-z-G · Mid-flow findings save (no status change).
     * PC / FS uses this to record what they found before formally resolving.
     */
    public function saveFindings(int $coId, array $d): void
    {
        $co = $this->find($coId);
        if (empty($co)) throw new RuntimeException('Callout not found.');
        if (in_array($co['status'], ['CLOSED','CANCELLED','CLIENT_ACCEPTED'], true)) {
            throw new RuntimeException('Cannot edit findings after client-accept / close.');
        }
        (new DB())->insertUpdate(
            "UPDATE callouts SET
                investigation_notes = :inv,
                root_cause          = :rca,
                snags               = :sn,
                edited_on = NOW()
              WHERE c0d = :id",
            [
                ':inv' => trim((string)($d['investigation_notes'] ?? '')) ?: null,
                ':rca' => trim((string)($d['root_cause']          ?? '')) ?: null,
                ':sn'  => trim((string)($d['snags']               ?? '')) ?: null,
                ':id'  => $coId,
            ]
        );
    }

    /** Client signs off on the resolution — status RESOLVED → CLIENT_ACCEPTED. */
    public function clientAccept(int $coId, string $clientName, ?string $signaturePath): void
    {
        $co = $this->find($coId);
        if (empty($co)) throw new RuntimeException('Callout not found.');
        if ($co['status'] !== 'RESOLVED') throw new RuntimeException('Only RESOLVED callouts can be accepted (status ' . $co['status'] . ').');
        (new DB())->insertUpdate(
            "UPDATE callouts SET
                status = 'CLIENT_ACCEPTED',
                client_accepted_by_name = :n,
                client_signature_path   = :sp,
                edited_on = NOW()
              WHERE c0d = :id",
            [':n' => trim($clientName) ?: null, ':sp' => trim((string)$signaturePath) ?: null, ':id' => $coId]
        );
    }

    /** Final terminal step — RESOLVED or CLIENT_ACCEPTED → CLOSED. Stamps closed_at. */
    public function close(int $coId, int $userId, string $userName): void
    {
        $co = $this->find($coId);
        if (empty($co)) throw new RuntimeException('Callout not found.');
        if (!in_array($co['status'], ['RESOLVED','CLIENT_ACCEPTED'], true)) {
            throw new RuntimeException('Only RESOLVED or CLIENT_ACCEPTED callouts can be closed (status ' . $co['status'] . ').');
        }
        (new DB())->insertUpdate(
            "UPDATE callouts SET
                status = 'CLOSED',
                closed_at = NOW(),
                closed_by = :u, closed_by_name = :un,
                edited_on = NOW()
              WHERE c0d = :id",
            [':u' => $userId ?: null, ':un' => trim($userName) ?: null, ':id' => $coId]
        );
    }

    /**
     * Slice 8Z-z-G · Spare requests raised from this callout — cross-links the
     * 8Z-z-D chain. Used by the "Related requests" card on the callout view.
     */
    public function linkedSpareRequests(int $coId): array
    {
        return (new DB())->mySelect(
            "SELECT r.c0d, r.request_no, r.urgency, r.who_pays, r.status, r.reason,
                    r.procurement_decision, r.created_on, r.site_id,
                    (SELECT COUNT(*) FROM spare_request_items i WHERE i.request_id = r.c0d AND i.deleted = 0) AS item_count
               FROM spare_requests r
              WHERE r.callout_id = :c AND r.deleted = 0
              ORDER BY r.created_on DESC, r.c0d DESC",
            [':c' => $coId]
        ) ?: [];
    }

    public function addPart(int $coId, array $d): int
    {
        $partId = (int)($d['part_id'] ?? 0);
        $qty    = (float)($d['qty'] ?? 0);
        if ($partId <= 0 || $qty <= 0) throw new InvalidArgumentException('part_id + qty required.');
        $src = strtoupper((string)($d['source'] ?? 'STOCK'));
        if (!in_array($src, ['STOCK','PROCURED','CLIENT_SUPPLIED','ADHOC'], true)) $src = 'STOCK';
        $db = new DB();
        $p = $db->mySelect("SELECT unit_cost FROM spare_parts WHERE c0d = :id", [':id' => $partId]);
        $cost = !empty($p) && $p[0]['unit_cost'] !== null ? (float)$p[0]['unit_cost'] : null;
        $fb = $db->insertUpdate(
            "INSERT INTO callout_parts_used (callout_id, part_id, qty, unit_cost_at_use, source, notes)
             VALUES (:c, :pt, :q, :uc, :s, :n)",
            [':c'=>$coId, ':pt'=>$partId, ':q'=>$qty, ':uc'=>$cost, ':s'=>$src, ':n'=>trim((string)($d['notes'] ?? '')) ?: null]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    /* ── Slice 8S-7 · Photo evidence ─────────────────────────────────── */

    private const MAX_PHOTO_BYTES = 10 * 1024 * 1024;
    private const OK_PHOTO_MIMES = ['image/jpeg','image/png','image/webp','image/gif','application/pdf'];

    public function photosFor(int $coId, ?string $kind = null): array
    {
        $db = new DB();
        $where = "WHERE callout_id = :c";
        $params = [':c' => $coId];
        if ($kind !== null) { $where .= " AND kind = :k"; $params[':k'] = strtoupper($kind); }
        return $db->mySelect("SELECT * FROM callout_photos $where ORDER BY c0d", $params) ?: [];
    }

    public function findPhoto(int $id): array
    {
        $db = new DB();
        $r  = $db->mySelect("SELECT * FROM callout_photos WHERE c0d = :id", [':id' => $id]);
        return $r[0] ?? [];
    }

    public function addPhoto(int $coId, array $file, string $kind, string $caption, int $uid, string $uname): int
    {
        $co = $this->find($coId);
        if (empty($co)) throw new RuntimeException('Callout not found.');
        [$rel, $orig, $mime, $size] = $this->stashPhoto($file, $coId);
        $kindU = strtoupper($kind);
        if (!in_array($kindU, ['SITE','ARRIVAL','FAULT','RESOLUTION','CLIENT_SIGNATURE','OTHER'], true)) $kindU = 'OTHER';
        $db = new DB();
        $fb = $db->insertUpdate(
            "INSERT INTO callout_photos
                (callout_id, file_path, original_name, mime, size_bytes, kind, caption, uploaded_by, uploaded_by_name)
             VALUES (:c, :fp, :on, :mm, :sz, :k, :cap, :u, :un)",
            [':c'=>$coId, ':fp'=>$rel, ':on'=>$orig, ':mm'=>$mime, ':sz'=>$size,
             ':k'=>$kindU, ':cap'=>trim($caption) ?: null, ':u'=>$uid, ':un'=>$uname]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    public function removePhoto(int $photoId): void
    {
        $p = $this->findPhoto($photoId);
        if (empty($p)) return;
        $abs = ROOT . '/' . ltrim((string)$p['file_path'], '/');
        if (is_file($abs)) @unlink($abs);
        $db = new DB();
        $db->insertUpdate("DELETE FROM callout_photos WHERE c0d = :id", [':id' => $photoId]);
    }

    private function stashPhoto(array $file, int $coId): array
    {
        $err = (int)($file['error'] ?? 0);
        if ($err !== UPLOAD_ERR_OK)                        throw new RuntimeException('Upload error ' . $err);
        $size = (int)($file['size'] ?? 0);
        if ($size <= 0 || $size > self::MAX_PHOTO_BYTES)   throw new RuntimeException('Photo size invalid.');
        $mime = @mime_content_type($file['tmp_name']) ?: (string)($file['type'] ?? '');
        if (!in_array($mime, self::OK_PHOTO_MIMES, true))  throw new RuntimeException('Unsupported photo type: ' . $mime);
        $orig = (string)($file['name'] ?? 'photo');
        $safe = preg_replace('/[^A-Za-z0-9._\-]+/', '_', $orig);
        $ext  = pathinfo($safe, PATHINFO_EXTENSION);
        $base = pathinfo($safe, PATHINFO_FILENAME) . '_' . substr(bin2hex(random_bytes(4)), 0, 8) . ($ext ? '.' . $ext : '');
        $dir  = ROOT . '/attachments/callout-photos/' . $coId;
        if (!is_dir($dir) && !@mkdir($dir, 0777, true) && !is_dir($dir)) throw new RuntimeException('Could not create attachment folder.');
        $abs = $dir . '/' . $base;
        if (!@move_uploaded_file($file['tmp_name'], $abs))              throw new RuntimeException('Failed to save photo.');
        return ['attachments/callout-photos/' . $coId . '/' . $base, $orig, $mime, $size];
    }
}
