<?php
namespace FreePBX\modules;

class Customastdb extends \FreePBX_Helpers implements \BMO {

    private $astdbPath = '/var/lib/asterisk/astdb.sqlite3';

    public function __construct($freepbx = null) {
        if ($freepbx == null) {
            throw new \Exception("Not given a FreePBX Object");
        }
        $this->FreePBX = $freepbx;
        $this->db     = $freepbx->Database;
    }

    // =====================
    // BMO required
    // =====================

    public function install() {}
    public function uninstall() { return true; }
    public function backup() {}
    public function restore($backup) {}

    public function doConfigPageInit($page) {}

    public function showPage() {
        $rows    = $this->getAllStates();
        $summary = $this->summarize($rows);
        ob_start();
        include __DIR__ . '/views/main.php';
        return ob_get_clean();
    }

    // =====================
    // Extensions edit-page integration is in functions.inc.php as legacy
    // `customastdb_users_configpageinit / _configpageload / _configprocess`
    // functions. FreePBX 17 BMO has no `getCoreExtensionTabs` hook — tabs on
    // the Extensions edit page are added the legacy way only.
    // =====================

    // =====================
    // AJAX dispatcher (status page filter + CSV export)
    // =====================

    public function ajaxRequest($req, &$setting) {
        $allowed = ['status', 'csv'];
        if (in_array($req, $allowed)) {
            $setting['authenticate'] = true;
            $setting['allowremote']  = false;
            return true;
        }
        return false;
    }

    public function ajaxCustomHandler() {
        if (($_REQUEST['command'] ?? '') === 'csv') {
            $this->exportCsv();
            return null;  // exportCsv exits directly
        }
        return false;
    }

    public function ajaxHandler() {
        $cmd = $_REQUEST['command'] ?? '';
        switch ($cmd) {
            case 'status':
                $rows = $this->getAllStates();
                return [
                    'rows'    => $rows,
                    'summary' => $this->summarize($rows),
                ];
            default:
                return ['error' => 'Unknown command'];
        }
    }

    private function exportCsv() {
        $rows = $this->getAllStates();
        $fname = 'astdb_state_' . date('Y-m-d_His') . '.csv';
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $fname . '"');
        header('Pragma: no-cache');
        $fp = fopen('php://output', 'w');
        fputcsv($fp, ['Extension', 'Name', 'DND', 'CF', 'CFB', 'CFU']);
        foreach ($rows as $r) {
            fputcsv($fp, [
                $r['extension'],
                $r['name'],
                $r['dnd'] ? 'YES' : '',
                $r['cf'],
                $r['cfb'],
                $r['cfu'],
            ]);
        }
        fclose($fp);
        exit;
    }

    // =====================
    // Bulk state reader — direct sqlite for speed
    // =====================

    /**
     * One row per known extension joined against the live astdb. Faster than
     * 4 AMI database_show calls and avoids the per-extension fetch loop.
     * Returns: [['extension' => '1001', 'name' => 'John', 'dnd' => true, 'cf' => '5551234', 'cfb' => '', 'cfu' => ''], ...]
     */
    public function getAllStates() {
        // 1. All extensions from FreePBX
        $stmt = $this->db->query("SELECT extension, name FROM users ORDER BY extension + 0, extension");
        $exts = $stmt->fetchAll(\PDO::FETCH_ASSOC);

        // 2. Pull DND/CF/CFB/CFU rows from astdb in one shot.
        // We scan the whole astdb (typically only a few hundred rows on a
        // healthy PBX) and filter in PHP. Empirically PDO sqlite gave partial
        // results when the WHERE used chained LIKE clauses with ORs — fetched
        // 1 of 3 matching rows that a CLI sqlite3 query returned in full. The
        // straight SELECT + PHP regex filter sidesteps that and is still O(N)
        // over a small file.
        $byExt = [];
        if (is_readable($this->astdbPath)) {
            try {
                $pdo = new \PDO('sqlite:' . $this->astdbPath);
                $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
                $rs = $pdo->query("SELECT key, value FROM astdb");
                while ($row = $rs->fetch(\PDO::FETCH_ASSOC)) {
                    if (!preg_match('#^/(DND|CFU|CFB|CF)/([^/]+)$#', $row['key'], $m)) continue;
                    $byExt[$m[2]][$m[1]] = $row['value'];
                }
            } catch (\Exception $e) {
                error_log("CustomAstDB: sqlite read failed: " . $e->getMessage());
            }
        }

        $out = [];
        foreach ($exts as $e) {
            $ext = (string)$e['extension'];
            $bag = $byExt[$ext] ?? [];
            $out[] = [
                'extension' => $ext,
                'name'      => (string)$e['name'],
                'dnd'       => !empty($bag['DND']),
                'cf'        => (string)($bag['CF']  ?? ''),
                'cfb'       => (string)($bag['CFB'] ?? ''),
                'cfu'       => (string)($bag['CFU'] ?? ''),
            ];
        }
        return $out;
    }

    /**
     * Per-extension fetch — used by the Extensions tab + UCP widget.
     *
     * NB: Asterisk flushes astdb writes to the sqlite file with a small lag
     * (~1-3 seconds in testing). For per-extension reads we go straight to
     * AMI via the core helpers, which always reflect the live in-memory
     * state — important when the admin saves the tab and reloads, or when
     * the UCP widget polls right after a write.
     */
    public function getStateForExtension($extension) {
        $extension = (string)$extension;
        $row = [
            'extension' => $extension,
            'dnd'       => false,
            'cf'        => '',
            'cfb'       => '',
            'cfu'       => '',
        ];
        try {
            $dnd = \FreePBX::Donotdisturb()->getStatusByExtension($extension);
            $row['dnd'] = ($dnd === 'YES');
        } catch (\Exception $e) {
            error_log("CustomAstDB: AMI DND read failed for $extension: " . $e->getMessage());
        }
        try {
            $cf = \FreePBX::Callforward()->getStatusesByExtension($extension);
            // getStatusesByExtension returns ['CF'=>..., 'CFU'=>..., 'CFB'=>...] — false when unset
            $row['cf']  = $cf['CF']  ? (string)$cf['CF']  : '';
            $row['cfb'] = $cf['CFB'] ? (string)$cf['CFB'] : '';
            $row['cfu'] = $cf['CFU'] ? (string)$cf['CFU'] : '';
        } catch (\Exception $e) {
            error_log("CustomAstDB: AMI CF read failed for $extension: " . $e->getMessage());
        }
        return $row;
    }

    public function summarize(array $rows) {
        $total = count($rows);
        $dnd = 0; $cfAny = 0; $clear = 0;
        foreach ($rows as $r) {
            $hasCf = ($r['cf'] !== '' || $r['cfb'] !== '' || $r['cfu'] !== '');
            if ($r['dnd']) $dnd++;
            if ($hasCf)    $cfAny++;
            if (!$r['dnd'] && !$hasCf) $clear++;
        }
        return [
            'total'      => $total,
            'with_dnd'   => $dnd,
            'with_cf'    => $cfAny,
            'clear'      => $clear,
        ];
    }

    // =====================
    // Write helpers — wrap Donotdisturb + Callforward for the BLF side-effects
    // =====================

    /**
     * Set or clear DND for an extension. $enabled boolean.
     * Routes through Donotdisturb::setStatusByExtension so AST_FUNC_DEVICE_STATE
     * BLF updates fire for every device assigned to the extension.
     */
    public function setDnd($extension, $enabled) {
        try {
            \FreePBX::Donotdisturb()->setStatusByExtension($extension, $enabled ? 'YES' : '');
            return true;
        } catch (\Exception $e) {
            error_log("CustomAstDB: setDnd($extension, " . ($enabled ? 'YES' : 'OFF') . ") failed: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Set or clear a Call Forward of a given type. Empty $number clears.
     * Note Callforward::setNumberByExtension signature is ($ext, $number, $type='CF')
     * — number is second arg, type is third (verified in core source).
     */
    public function setCf($extension, $type, $number) {
        $type = strtoupper($type);
        if (!in_array($type, ['CF', 'CFB', 'CFU'], true)) return false;
        try {
            if ($number === '' || $number === null) {
                \FreePBX::Callforward()->delNumberByExtension($extension, $type);
            } else {
                \FreePBX::Callforward()->setNumberByExtension($extension, $number, $type);
            }
            return true;
        } catch (\Exception $e) {
            error_log("CustomAstDB: setCf($extension, $type, '$number') failed: " . $e->getMessage());
            return false;
        }
    }
}
