<?php
/**
 * ProjectDocuments — file attachments hung on a c_projects row.
 *
 * Slice 8Y · PM (or Admin) uploads the actual signed Client PO document + any supporting
 * artefacts (BOQ, drawings, specs, warranty terms). Files land under
 *   attachments/projects/{project_id}/
 * with a random suffix to avoid collision, original filename preserved for downloads.
 *
 * MIME whitelist: PDF + common image + Excel / Word (contract packs sometimes bundle multiple).
 * 15 MB hard cap so we don't accept unbounded uploads.
 */
class ProjectDocuments extends Model
{
    private const MAX_BYTES = 15 * 1024 * 1024;
    private const KINDS     = ['CLIENT_PO','BOQ','DRAWING','SPEC','WARRANTY','OTHER'];
    private const MIME_OK = [
        'application/pdf', 'image/jpeg', 'image/png', 'image/webp', 'image/gif',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'application/vnd.ms-excel',
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'text/csv', 'text/plain',
    ];

    public function all(int $projectId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT * FROM c_projects_documents WHERE project_id = :p AND deleted = 0 ORDER BY c0d DESC",
            [':p' => $projectId]
        ) ?: [];
    }

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

    /**
     * Persist an uploaded file. Called with the $_FILES entry.
     * Returns the new doc id. Throws on validation failure.
     */
    public function upload(int $projectId, string $docKind, array $file, string $notes, int $userId, string $userName): int
    {
        if ($projectId <= 0)                        throw new InvalidArgumentException('Missing project id.');
        if (!in_array($docKind, self::KINDS, true)) throw new InvalidArgumentException('Invalid document kind.');
        if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
            throw new RuntimeException('No file uploaded.');
        }
        $err = (int)($file['error'] ?? 0);
        if ($err !== UPLOAD_ERR_OK) throw new RuntimeException('Upload error (code ' . $err . ').');
        $size = (int)($file['size'] ?? 0);
        if ($size <= 0)                    throw new RuntimeException('Uploaded file is empty.');
        if ($size > self::MAX_BYTES)       throw new RuntimeException('File too large (max 15 MB).');

        // Detect MIME server-side (client-provided is untrustworthy).
        $mime = @mime_content_type($file['tmp_name']) ?: (string)($file['type'] ?? '');
        if (!in_array($mime, self::MIME_OK, true)) {
            throw new RuntimeException('Unsupported file type: ' . $mime);
        }

        // Sanitise + destination
        $orig = (string)($file['name'] ?? 'file');
        $safe = preg_replace('/[^A-Za-z0-9._\-]+/', '_', $orig);
        $ext  = pathinfo($safe, PATHINFO_EXTENSION);
        $suffix = substr(bin2hex(random_bytes(4)), 0, 8);
        $baseName = pathinfo($safe, PATHINFO_FILENAME) . '_' . $suffix . ($ext ? ('.' . $ext) : '');

        $dir = ROOT . '/attachments/projects/' . $projectId;
        if (!is_dir($dir) && !@mkdir($dir, 0777, true) && !is_dir($dir)) {
            throw new RuntimeException('Could not create attachments directory.');
        }
        $absPath = $dir . '/' . $baseName;
        if (!@move_uploaded_file($file['tmp_name'], $absPath)) {
            throw new RuntimeException('Failed to save uploaded file.');
        }
        // Store relative path (from ROOT) so downloads can rebuild the absolute path.
        $relPath = 'attachments/projects/' . $projectId . '/' . $baseName;

        $db = new DB();
        $fb = $db->insertUpdate(
            "INSERT INTO c_projects_documents
                (project_id, doc_kind, file_path, original_name, mime, size_bytes, notes,
                 uploaded_by, uploaded_by_name, created_on)
             VALUES (:p, :k, :fp, :on, :m, :sz, :nt, :ub, :ubn, NOW())",
            [
                ':p'  => $projectId, ':k' => $docKind, ':fp' => $relPath, ':on' => $orig,
                ':m'  => $mime, ':sz' => $size, ':nt' => trim($notes),
                ':ub' => $userId, ':ubn' => $userName,
            ]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    /**
     * Soft-delete. File stays on disk (audit / recovery) but no longer shown.
     * Only the uploader or an Administrator can delete.
     */
    public function softDelete(int $docId, int $userId, string $userGroup): void
    {
        $d = $this->find($docId);
        if (!$d) throw new RuntimeException('Document not found.');
        $g = strtoupper(trim($userGroup));
        if ((int)$d['uploaded_by'] !== $userId && $g !== 'ADMINISTRATOR') {
            throw new RuntimeException('Only the uploader or an Administrator can delete this document.');
        }
        $db = new DB();
        $db->insertUpdate(
            "UPDATE c_projects_documents SET deleted = 1, edited_on = NOW() WHERE c0d = :id",
            [':id' => $docId]
        );
    }
}
