<?php
namespace FreePBX\modules;

class Customcdr extends \FreePBX_Helpers implements \BMO {

    private $cdrdb = null;

    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 Methods
    // =====================

    public function install() {}
    public function uninstall() {}
    public function backup() {}
    public function restore($backup) {}

    public function doConfigPageInit($page) {
        // Read-only module — no form handling
    }

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

    // =====================
    // CDR Database Connection
    // =====================

    private function connectCdrDb() {
        if ($this->cdrdb !== null) {
            return $this->cdrdb;
        }

        global $amp_conf;

        // CDR-specific vars override AMP vars; fall back to $amp_conf global
        $host = !empty($amp_conf['CDRDBHOST']) ? $amp_conf['CDRDBHOST']
              : (!empty($amp_conf['AMPDBHOST']) ? $amp_conf['AMPDBHOST'] : 'localhost');
        $name = !empty($amp_conf['CDRDBNAME']) ? $amp_conf['CDRDBNAME'] : 'asteriskcdrdb';
        $user = !empty($amp_conf['CDRDBUSER']) ? $amp_conf['CDRDBUSER']
              : (!empty($amp_conf['AMPDBUSER']) ? $amp_conf['AMPDBUSER'] : 'freepbxuser');
        $pass = isset($amp_conf['CDRDBPASS']) && $amp_conf['CDRDBPASS'] !== ''
              ? $amp_conf['CDRDBPASS']
              : (isset($amp_conf['AMPDBPASS']) ? $amp_conf['AMPDBPASS'] : '');
        $port = !empty($amp_conf['CDRDBPORT']) ? $amp_conf['CDRDBPORT']
              : (!empty($amp_conf['AMPDBPORT']) ? $amp_conf['AMPDBPORT'] : '3306');

        $dsn = "mysql:host={$host};port={$port};dbname={$name};charset=utf8";
        $this->cdrdb = new \PDO($dsn, $user, $pass, [
            \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
        ]);

        return $this->cdrdb;
    }

    // =====================
    // AJAX Handlers
    // =====================

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

    public function ajaxCustomHandler() {
        if (($_REQUEST['command'] ?? '') === 'serveRecording') {
            $file = $_REQUEST['file'] ?? '';
            $calldate = $_REQUEST['calldate'] ?? '';
            $download = !empty($_REQUEST['download']);

            if (!empty($file)) {
                $fullPath = $this->resolveRecordingPath($file, $calldate);
                if ($fullPath !== null && is_file($fullPath)) {
                    $basename = basename($fullPath);
                    $ext = strtolower(pathinfo($basename, PATHINFO_EXTENSION));
                    $mimeMap = [
                        'wav' => 'audio/wav',
                        'mp3' => 'audio/mpeg',
                        'ogg' => 'audio/ogg',
                        'gsm' => 'audio/x-gsm',
                    ];
                    $mime = isset($mimeMap[$ext]) ? $mimeMap[$ext] : 'application/octet-stream';

                    header('Content-Type: ' . $mime);
                    header('Content-Length: ' . filesize($fullPath));
                    header('Accept-Ranges: bytes');
                    if ($download) {
                        header('Content-Disposition: attachment; filename="' . $basename . '"');
                    } else {
                        header('Content-Disposition: inline; filename="' . $basename . '"');
                    }
                    header('Cache-Control: no-cache');

                    // Support range requests for seeking
                    if (isset($_SERVER['HTTP_RANGE'])) {
                        $size = filesize($fullPath);
                        preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m);
                        $start = intval($m[1]);
                        $end = !empty($m[2]) ? intval($m[2]) : $size - 1;
                        header('HTTP/1.1 206 Partial Content');
                        header("Content-Range: bytes $start-$end/$size");
                        header('Content-Length: ' . ($end - $start + 1));
                        $fp = fopen($fullPath, 'rb');
                        fseek($fp, $start);
                        echo fread($fp, $end - $start + 1);
                        fclose($fp);
                    } else {
                        readfile($fullPath);
                    }
                    exit;
                }
            }
            header('HTTP/1.1 404 Not Found');
            echo 'Recording not found';
            exit;
        }
        return false;
    }

    public function ajaxHandler() {
        $command = $_REQUEST['command'] ?? '';

        switch ($command) {
            case 'getCdrData':
                return $this->getCdrData($_REQUEST);
            case 'exportCsv':
                $this->exportCsv($_REQUEST);
                return null; // exportCsv exits directly
            case 'getFilterOptions':
                return $this->getFilterOptions();
            default:
                return ['error' => 'Unknown command'];
        }
    }

    /**
     * Resolve a recording filename to its full path on disk.
     * Recordings may be in the base monitor dir or in YYYY/MM/DD subdirectories.
     */
    private function resolveRecordingPath($file, $calldate = '') {
        global $amp_conf;
        $monitorDir = !empty($amp_conf['ASTSPOOLDIR'])
            ? $amp_conf['ASTSPOOLDIR'] . '/monitor'
            : '/var/spool/asterisk/monitor';

        // If the file is already a full path that exists, use it
        if (strpos($file, '/') === 0 && file_exists($file)) {
            return $file;
        }

        // Try base monitor dir
        if (file_exists($monitorDir . '/' . $file)) {
            return $monitorDir . '/' . $file;
        }

        // Try date-based subdirectory from calldate (YYYY/MM/DD)
        if (!empty($calldate)) {
            $ts = strtotime($calldate);
            if ($ts !== false) {
                $dateDir = date('Y/m/d', $ts);
                $path = $monitorDir . '/' . $dateDir . '/' . $file;
                if (file_exists($path)) {
                    return $path;
                }
            }
        }

        // Fallback: try to extract date from filename pattern
        // e.g. out-1312342-2013-20260227-105308-... → 2026/02/27
        if (preg_match('/(\d{4})(\d{2})(\d{2})-\d{6}-/', $file, $m)) {
            $path = $monitorDir . '/' . $m[1] . '/' . $m[2] . '/' . $m[3] . '/' . $file;
            if (file_exists($path)) {
                return $path;
            }
        }

        return null;
    }

    // =====================
    // Query Orchestrator
    // =====================

    private function getCdrData($request) {
        try {
            $db = $this->connectCdrDb();
            $filters = $this->parseFilters($request);
            list($where, $params) = $this->buildWhereClause($filters);

            $page = max(1, intval($request['page'] ?? 1));
            $pageSize = max(10, min(200, intval($this->getSetting('page_size') ?: 50)));
            $sortCol = $request['sort'] ?? 'calldate';
            $sortDir = $request['dir'] ?? 'DESC';

            $stats = $this->queryStats($where, $params);
            $charts = $this->queryCharts($where, $params);
            $rows = $this->queryRows($where, $params, $page, $pageSize, $sortCol, $sortDir);

            // Total row count for pagination
            $countSql = "SELECT COUNT(*) FROM cdr $where";
            $stmt = $db->prepare($countSql);
            $stmt->execute($params);
            $totalRows = (int)$stmt->fetchColumn();

            return [
                'stats'      => $stats,
                'charts'     => $charts,
                'rows'       => $rows,
                'pagination' => [
                    'page'       => $page,
                    'pageSize'   => $pageSize,
                    'totalRows'  => $totalRows,
                    'totalPages' => max(1, ceil($totalRows / $pageSize)),
                ],
            ];
        } catch (\Exception $e) {
            return ['error' => $e->getMessage()];
        }
    }

    // =====================
    // Filter Parsing
    // =====================

    private function parseFilters($request) {
        $filters = [];

        if (!empty($request['date_from'])) {
            $filters['date_from'] = $request['date_from'];
        }
        if (!empty($request['date_to'])) {
            $filters['date_to'] = $request['date_to'];
        }
        if (!empty($request['extension']) && strtolower(trim($request['extension'])) !== 'all') {
            $filters['extension'] = trim($request['extension']);
        }
        if (!empty($request['did']) && strtolower(trim($request['did'])) !== 'all') {
            $filters['did'] = trim($request['did']);
        }
        if (!empty($request['direction']) && $request['direction'] !== 'all') {
            $filters['direction'] = $request['direction'];
        }
        if (!empty($request['disposition']) && $request['disposition'] !== 'all') {
            $filters['disposition'] = strtoupper($request['disposition']);
        }
        if (!empty($request['search'])) {
            $filters['search'] = trim($request['search']);
        }

        return $filters;
    }

    private function buildWhereClause($filters) {
        $conditions = [];
        $params = [];

        if (!empty($filters['date_from'])) {
            $conditions[] = "calldate >= ?";
            $params[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $conditions[] = "calldate <= ?";
            $params[] = $filters['date_to'];
        }
        if (!empty($filters['extension'])) {
            $conditions[] = "(src = ? OR dst = ? OR cnum = ?)";
            $params[] = $filters['extension'];
            $params[] = $filters['extension'];
            $params[] = $filters['extension'];
        }
        if (!empty($filters['did'])) {
            $conditions[] = "did LIKE ?";
            $params[] = '%' . $filters['did'] . '%';
        }
        if (!empty($filters['disposition'])) {
            $conditions[] = "disposition = ?";
            $params[] = $filters['disposition'];
        }
        if (!empty($filters['direction'])) {
            switch ($filters['direction']) {
                case 'inbound':
                    $conditions[] = "(dcontext LIKE '%from-trunk%' OR dcontext LIKE '%from-did%' OR dcontext LIKE '%from-pstn%')";
                    break;
                case 'outbound':
                    $conditions[] = "(dcontext LIKE '%from-internal%' AND LENGTH(dst) > 5)";
                    break;
                case 'internal':
                    $conditions[] = "((dcontext LIKE '%from-internal%' OR dcontext LIKE '%ext-local%') AND LENGTH(dst) <= 5)";
                    break;
            }
        }
        if (!empty($filters['search'])) {
            $conditions[] = "(src LIKE ? OR dst LIKE ? OR clid LIKE ? OR did LIKE ? OR channel LIKE ? OR dstchannel LIKE ?)";
            $searchTerm = '%' . $filters['search'] . '%';
            $params[] = $searchTerm;
            $params[] = $searchTerm;
            $params[] = $searchTerm;
            $params[] = $searchTerm;
            $params[] = $searchTerm;
            $params[] = $searchTerm;
        }

        $where = '';
        if (!empty($conditions)) {
            $where = 'WHERE ' . implode(' AND ', $conditions);
        }

        return [$where, $params];
    }

    // =====================
    // Statistics Query
    // =====================

    private function queryStats($where, $params) {
        $db = $this->connectCdrDb();

        $sql = "SELECT
            COUNT(*) AS total_calls,
            SUM(CASE WHEN disposition = 'ANSWERED' THEN 1 ELSE 0 END) AS answered,
            SUM(CASE WHEN disposition = 'NO ANSWER' THEN 1 ELSE 0 END) AS no_answer,
            SUM(CASE WHEN disposition = 'BUSY' THEN 1 ELSE 0 END) AS busy,
            SUM(CASE WHEN disposition = 'FAILED' THEN 1 ELSE 0 END) AS failed,
            SUM(duration) AS total_duration,
            SUM(billsec) AS total_billsec,
            AVG(CASE WHEN disposition = 'ANSWERED' THEN billsec ELSE NULL END) AS avg_duration
        FROM cdr $where";

        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        $row = $stmt->fetch();

        return [
            'total_calls'   => (int)($row['total_calls'] ?? 0),
            'answered'       => (int)($row['answered'] ?? 0),
            'no_answer'      => (int)($row['no_answer'] ?? 0),
            'busy'           => (int)($row['busy'] ?? 0),
            'failed'         => (int)($row['failed'] ?? 0),
            'total_duration' => (int)($row['total_duration'] ?? 0),
            'total_billsec'  => (int)($row['total_billsec'] ?? 0),
            'avg_duration'   => round((float)($row['avg_duration'] ?? 0)),
        ];
    }

    // =====================
    // Chart Queries
    // =====================

    private function queryCharts($where, $params) {
        $db = $this->connectCdrDb();

        // 1. Volume by Day (stacked by disposition)
        $sql = "SELECT DATE(calldate) AS day, disposition, COUNT(*) AS cnt
                FROM cdr $where
                GROUP BY DATE(calldate), disposition
                ORDER BY day";
        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        $volumeByDay = $stmt->fetchAll();

        // 2. Top 10 Extensions
        $extAnd = $where ? ' AND' : ' WHERE';
        $sql = "SELECT ext, SUM(cnt) AS total FROM (
                    SELECT src AS ext, COUNT(*) AS cnt FROM cdr $where
                    $extAnd src REGEXP '^[0-9]{3,5}$'
                    GROUP BY src
                    UNION ALL
                    SELECT dst AS ext, COUNT(*) AS cnt FROM cdr $where
                    $extAnd dst REGEXP '^[0-9]{3,5}$'
                    GROUP BY dst
                ) combined
                GROUP BY ext ORDER BY total DESC LIMIT 10";
        // Need to double the params for UNION ALL
        $stmt = $db->prepare($sql);
        $stmt->execute(array_merge($params, $params));
        $topExtensions = $stmt->fetchAll();

        // 3. Disposition Pie
        $sql = "SELECT disposition, COUNT(*) AS cnt
                FROM cdr $where
                GROUP BY disposition
                ORDER BY cnt DESC";
        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        $dispositionPie = $stmt->fetchAll();

        // 4. Top 10 DIDs
        $didWhere = $where ? "$where AND did != ''" : "WHERE did != ''";
        $sql = "SELECT did, COUNT(*) AS cnt
                FROM cdr $didWhere
                GROUP BY did
                ORDER BY cnt DESC LIMIT 10";
        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        $topDids = $stmt->fetchAll();

        // 5. Hourly Distribution
        $sql = "SELECT HOUR(calldate) AS hr, COUNT(*) AS cnt
                FROM cdr $where
                GROUP BY HOUR(calldate)
                ORDER BY hr";
        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        $hourly = $stmt->fetchAll();

        return [
            'volume_by_day'   => $volumeByDay,
            'top_extensions'  => $topExtensions,
            'disposition_pie' => $dispositionPie,
            'top_dids'        => $topDids,
            'hourly'          => $hourly,
        ];
    }

    // =====================
    // Paginated Rows
    // =====================

    private function queryRows($where, $params, $page, $pageSize, $sortCol, $sortDir) {
        $db = $this->connectCdrDb();

        list($safeCol, $safeDir) = $this->sanitizeSort($sortCol, $sortDir);
        $offset = ($page - 1) * $pageSize;

        $directionCase = "CASE
            WHEN dcontext LIKE '%from-trunk%' OR dcontext LIKE '%from-did%'
                 OR dcontext LIKE '%from-pstn%' THEN 'Inbound'
            WHEN (dcontext LIKE '%from-internal%' OR dcontext LIKE '%ext-local%')
                 AND LENGTH(dst) <= 5 THEN 'Internal'
            WHEN dcontext LIKE '%from-internal%' THEN 'Outbound'
            ELSE 'Other'
        END";

        $sql = "SELECT calldate, src, dst, did, dcontext, duration, billsec,
                       disposition, clid, channel, dstchannel, recordingfile,
                       $directionCase AS direction
                FROM cdr $where
                ORDER BY $safeCol $safeDir
                LIMIT $pageSize OFFSET $offset";

        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }

    private function sanitizeSort($column, $dir) {
        $allowed = ['calldate', 'src', 'dst', 'did', 'duration', 'billsec', 'disposition', 'direction'];
        $col = in_array($column, $allowed) ? $column : 'calldate';

        // direction is a computed column — need to use the CASE expression
        if ($col === 'direction') {
            $col = "CASE
                WHEN dcontext LIKE '%from-trunk%' OR dcontext LIKE '%from-did%'
                     OR dcontext LIKE '%from-pstn%' THEN 'Inbound'
                WHEN (dcontext LIKE '%from-internal%' OR dcontext LIKE '%ext-local%')
                     AND LENGTH(dst) <= 5 THEN 'Internal'
                WHEN dcontext LIKE '%from-internal%' THEN 'Outbound'
                ELSE 'Other'
            END";
        }

        $d = (strtoupper($dir) === 'ASC') ? 'ASC' : 'DESC';
        return [$col, $d];
    }

    // =====================
    // CSV Export
    // =====================

    private function exportCsv($request) {
        $db = $this->connectCdrDb();
        $filters = $this->parseFilters($request);
        list($where, $params) = $this->buildWhereClause($filters);

        $directionCase = "CASE
            WHEN dcontext LIKE '%from-trunk%' OR dcontext LIKE '%from-did%'
                 OR dcontext LIKE '%from-pstn%' THEN 'Inbound'
            WHEN (dcontext LIKE '%from-internal%' OR dcontext LIKE '%ext-local%')
                 AND LENGTH(dst) <= 5 THEN 'Internal'
            WHEN dcontext LIKE '%from-internal%' THEN 'Outbound'
            ELSE 'Other'
        END";

        $sql = "SELECT calldate, src, dst, did,
                       $directionCase AS direction,
                       duration, billsec, disposition, clid, channel, dstchannel
                FROM cdr $where
                ORDER BY calldate DESC
                LIMIT 100000";

        $stmt = $db->prepare($sql);
        $stmt->execute($params);

        $filename = 'cdr_export_' . date('Y-m-d_His') . '.csv';

        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Pragma: no-cache');
        header('Expires: 0');

        $fp = fopen('php://output', 'w');
        fputcsv($fp, ['Date', 'Source', 'Destination', 'DID', 'Direction',
                       'Duration', 'Billable', 'Disposition', 'Caller ID',
                       'Channel', 'Dest Channel']);

        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            fputcsv($fp, [
                $row['calldate'],
                $row['src'],
                $row['dst'],
                $row['did'],
                $row['direction'],
                $row['duration'],
                $row['billsec'],
                $row['disposition'],
                $row['clid'],
                $row['channel'],
                $row['dstchannel'],
            ]);
        }

        fclose($fp);
        exit;
    }

    // =====================
    // Filter Options (distinct extensions + DIDs)
    // =====================

    private function getFilterOptions() {
        try {
            $db = $this->connectCdrDb();

            // Distinct extensions: union of src and dst that look like extensions
            $sql = "SELECT DISTINCT ext FROM (
                        SELECT src AS ext FROM cdr WHERE src REGEXP '^[0-9]{3,5}$'
                        UNION
                        SELECT dst AS ext FROM cdr WHERE dst REGEXP '^[0-9]{3,5}$'
                    ) combined ORDER BY ext";
            $stmt = $db->query($sql);
            $extensions = $stmt->fetchAll(\PDO::FETCH_COLUMN);

            // Distinct DIDs
            $sql = "SELECT DISTINCT did FROM cdr WHERE did != '' AND did IS NOT NULL ORDER BY did";
            $stmt = $db->query($sql);
            $dids = $stmt->fetchAll(\PDO::FETCH_COLUMN);

            return ['extensions' => $extensions, 'dids' => $dids];
        } catch (\Exception $e) {
            return ['extensions' => [], 'dids' => [], 'error' => $e->getMessage()];
        }
    }

    // =====================
    // Config Helpers
    // =====================

    public function getSetting($key) {
        $sql = "SELECT config_value FROM customcdr_config WHERE config_key = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$key]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
        return $row ? $row['config_value'] : null;
    }

    public function setSetting($key, $value) {
        $sql = "INSERT INTO customcdr_config (config_key, config_value) VALUES (?, ?)
                ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$key, $value]);
    }

    public function getAllSettings() {
        $sql = "SELECT config_key, config_value FROM customcdr_config";
        $stmt = $this->db->query($sql);
        $config = [];
        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            $config[$row['config_key']] = $row['config_value'];
        }
        return $config;
    }
}
