<?php
/**
 * Family Hub MCP server (stdio, JSON-RPC). No Composer or Node.
 *
 * Env (set in Cursor MCP config):
 *   FAMILYHUB_BASE_URL  e.g. https://example.com/family-hub/api
 *   FAMILYHUB_MCP_TOKEN same value as MCP_API_TOKEN in .env
 */

declare(strict_types=1);

const FAMILYHUB_MCP_PROTOCOL_VERSION = '2024-11-05';

/** @var list<string> */
const FAMILYHUB_MCP_POST_ALLOWLIST = [
    'family_settings_save.php',
    'meal_ingredient_to_grocery.php',
    'meal_shopping_item_to_grocery.php',
    'meal_push_grocery.php',
    'meal_plan_set_slot.php',
    'meal_plan_set_week.php',
    'meal_delete.php',
    'meal_export.php',
    'meal_import.php',
    'meal_save.php',
    'grocery_item_create.php',
    'grocery_item_review.php',
    'grocery_item_delete.php',
    'grocery_item_purchase.php',
    'store_delete.php',
    'store_update.php',
    'store_create.php',
    'chore_save.php',
    'expense_create.php',
    'chore_review.php',
    'chore_submit.php',
    'chore_delete.php',
    'people_create_first.php',
    'people_update.php',
    'people_create.php',
    'people_reset_pin.php',
    'people_delete.php',
    'switch_person.php',
];

/** @var list<string> */
const FAMILYHUB_MCP_SNAPSHOT_TYPES = [
    'people',
    'chores',
    'stores',
    'grocery_lists',
    'grocery_catalog',
    'meals',
    'meal_plans',
    'family_settings',
    'expenses',
];

/**
 * @return array{base: string, token: string}
 */
function familyHubMcpEnv(): array {
    $base = rtrim((string) getenv('FAMILYHUB_BASE_URL'), '/');
    $token = (string) getenv('FAMILYHUB_MCP_TOKEN');
    return ['base' => $base, 'token' => $token];
}

/**
 * @param 'GET'|'POST' $method
 * @return array{ok: bool, status: int, body: string}
 */
function familyHubMcpHttp(string $method, string $pathAndQuery, ?array $jsonBody): array {
    $c = familyHubMcpEnv();
    if ($c['base'] === '' || $c['token'] === '') {
        return ['ok' => false, 'status' => 0, 'body' => 'Set FAMILYHUB_BASE_URL and FAMILYHUB_MCP_TOKEN in the MCP server environment.'];
    }
    $path = ltrim($pathAndQuery, '/');
    if ($path === '' || strpos($path, '..') !== false) {
        return ['ok' => false, 'status' => 0, 'body' => 'Invalid path.'];
    }
    $url = $c['base'] . '/' . $path;

    $headerLines = ['Authorization: Bearer ' . $c['token']];
    $content = '';
    if ($method === 'POST' && $jsonBody !== null) {
        $headerLines[] = 'Content-Type: application/json; charset=utf-8';
        $enc = json_encode($jsonBody, JSON_UNESCAPED_UNICODE);
        $content = $enc !== false ? $enc : '{}';
    }

    $ctx = stream_context_create([
        'http' => [
            'method' => $method,
            'header' => implode("\r\n", $headerLines),
            'content' => $content,
            'timeout' => 120,
            'ignore_errors' => true,
        ],
    ]);

    $result = @file_get_contents($url, false, $ctx);
    $status = 0;
    if (isset($http_response_header) && is_array($http_response_header)) {
        foreach ($http_response_header as $line) {
            if (preg_match('#^HTTP/\S+\s+(\d+)#', $line, $m)) {
                $status = (int) $m[1];
                break;
            }
        }
    }
    if ($result === false) {
        return ['ok' => false, 'status' => $status, 'body' => 'HTTP request failed for ' . $url];
    }
    return ['ok' => $status >= 200 && $status < 300, 'status' => $status, 'body' => $result];
}

function familyHubMcpWriteJsonrpc(array $payload): void {
    $json = json_encode($payload, JSON_UNESCAPED_UNICODE);
    if ($json === false) {
        return;
    }
    $out = 'Content-Length: ' . strlen($json) . "\r\n\r\n" . $json;
    fwrite(STDOUT, $out);
    fflush(STDOUT);
}

/**
 * @return array<string, mixed>|null
 */
function familyHubMcpReadMessage(): ?array {
    $headerLines = [];
    while (true) {
        $line = fgets(STDIN);
        if ($line === false) {
            return null;
        }
        $line = rtrim($line, "\r\n");
        if ($line === '') {
            break;
        }
        $headerLines[] = $line;
    }
    $contentLength = 0;
    foreach ($headerLines as $hl) {
        if (stripos($hl, 'Content-Length:') === 0) {
            $contentLength = (int) trim(substr($hl, strlen('Content-Length:')));
        }
    }
    if ($contentLength <= 0) {
        return null;
    }
    $body = '';
    while (strlen($body) < $contentLength) {
        $chunk = fread(STDIN, $contentLength - strlen($body));
        if ($chunk === false || $chunk === '') {
            return null;
        }
        $body .= $chunk;
    }
    $decoded = json_decode($body, true);
    return is_array($decoded) ? $decoded : null;
}

/**
 * @return array<int, array<string, mixed>>
 */
function familyHubMcpToolList(): array {
    $typesEnum = ['type' => 'string', 'enum' => FAMILYHUB_MCP_SNAPSHOT_TYPES];
    $postEnum = ['type' => 'string', 'enum' => FAMILYHUB_MCP_POST_ALLOWLIST];

    return [
        [
            'name' => 'familyhub_health',
            'description' => 'Check MCP configuration and API reachability via family_settings snapshot.',
            'inputSchema' => ['type' => 'object', 'properties' => new stdClass()],
        ],
        [
            'name' => 'familyhub_get_snapshot',
            'description' => 'Read allowlisted JSON domain data (people, chores, stores, grocery_lists, grocery_catalog, meals, meal_plans, family_settings, expenses).',
            'inputSchema' => [
                'type' => 'object',
                'properties' => ['type' => $typesEnum],
                'required' => ['type'],
            ],
        ],
        [
            'name' => 'familyhub_calendar_events',
            'description' => 'GET api/calendar_events.php — agenda for a date range. Omit from/to for server default window.',
            'inputSchema' => [
                'type' => 'object',
                'properties' => [
                    'from' => ['type' => 'string', 'description' => 'YYYY-MM-DD'],
                    'to' => ['type' => 'string', 'description' => 'YYYY-MM-DD'],
                ],
            ],
        ],
        [
            'name' => 'familyhub_post_api',
            'description' => 'POST JSON to an allowlisted api/*.php endpoint (same contract as the web UI). Body is the JSON object sent as the POST body.',
            'inputSchema' => [
                'type' => 'object',
                'properties' => [
                    'endpoint' => $postEnum,
                    'body' => ['type' => 'object', 'description' => 'JSON object payload'],
                ],
                'required' => ['endpoint', 'body'],
            ],
        ],
    ];
}

/**
 * @param array<string, mixed> $args
 * @return array{isError?: bool, content: list<array{type: string, text: string}>}
 */
function familyHubMcpRunTool(string $name, array $args): array {
    if ($name === 'familyhub_health') {
        $r = familyHubMcpHttp('GET', 'data_snapshot.php?type=family_settings', null);
        $text = $r['ok']
            ? $r['body']
            : 'HTTP ' . $r['status'] . ': ' . $r['body'];
        return ['content' => [['type' => 'text', 'text' => $text], ], 'isError' => !$r['ok']];
    }
    if ($name === 'familyhub_get_snapshot') {
        $type = isset($args['type']) ? trim((string) $args['type']) : '';
        if (!in_array($type, FAMILYHUB_MCP_SNAPSHOT_TYPES, true)) {
            return [
                'content' => [['type' => 'text', 'text' => 'type must be one of: ' . implode(', ', FAMILYHUB_MCP_SNAPSHOT_TYPES)]],
                'isError' => true,
            ];
        }
        $r = familyHubMcpHttp('GET', 'data_snapshot.php?type=' . rawurlencode($type), null);
        return [
            'content' => [['type' => 'text', 'text' => $r['body']]],
            'isError' => !$r['ok'],
        ];
    }
    if ($name === 'familyhub_calendar_events') {
        $from = isset($args['from']) ? trim((string) $args['from']) : '';
        $to = isset($args['to']) ? trim((string) $args['to']) : '';
        $q = 'calendar_events.php';
        if ($from !== '' && $to !== '') {
            $q .= '?from=' . rawurlencode($from) . '&to=' . rawurlencode($to);
        }
        $r = familyHubMcpHttp('GET', $q, null);
        return [
            'content' => [['type' => 'text', 'text' => $r['body']]],
            'isError' => !$r['ok'],
        ];
    }
    if ($name === 'familyhub_post_api') {
        $endpoint = isset($args['endpoint']) ? trim((string) $args['endpoint']) : '';
        if (!in_array($endpoint, FAMILYHUB_MCP_POST_ALLOWLIST, true)) {
            return [
                'content' => [['type' => 'text', 'text' => 'endpoint not allowlisted']],
                'isError' => true,
            ];
        }
        $body = $args['body'] ?? [];
        if (!is_array($body)) {
            $body = [];
        }
        $r = familyHubMcpHttp('POST', $endpoint, $body);
        return [
            'content' => [['type' => 'text', 'text' => $r['body']]],
            'isError' => !$r['ok'],
        ];
    }
    return [
        'content' => [['type' => 'text', 'text' => 'Unknown tool']],
        'isError' => true,
    ];
}

/**
 * @param array<string, mixed> $msg
 */
function familyHubMcpHandleMessage(array $msg): void {
    $method = isset($msg['method']) ? (string) $msg['method'] : '';
    $isNotification = !array_key_exists('id', $msg);
    $id = !$isNotification ? $msg['id'] : null;

    if ($method === 'notifications/initialized' || $method === 'notifications/cancelled') {
        return;
    }

    if ($method === 'initialize') {
        familyHubMcpWriteJsonrpc([
            'jsonrpc' => '2.0',
            'id' => $id,
            'result' => [
                'protocolVersion' => FAMILYHUB_MCP_PROTOCOL_VERSION,
                'capabilities' => ['tools' => new stdClass()],
                'serverInfo' => ['name' => 'familyhub', 'version' => '1.0.0'],
            ],
        ]);
        return;
    }

    if ($method === 'ping') {
        if (!$isNotification) {
            familyHubMcpWriteJsonrpc([
                'jsonrpc' => '2.0',
                'id' => $id,
                'result' => new stdClass(),
            ]);
        }
        return;
    }

    if ($method === 'tools/list') {
        if (!$isNotification) {
            familyHubMcpWriteJsonrpc([
                'jsonrpc' => '2.0',
                'id' => $id,
                'result' => ['tools' => familyHubMcpToolList()],
            ]);
        }
        return;
    }

    if ($method === 'resources/list') {
        if (!$isNotification) {
            familyHubMcpWriteJsonrpc([
                'jsonrpc' => '2.0',
                'id' => $id,
                'result' => ['resources' => []],
            ]);
        }
        return;
    }

    if ($method === 'prompts/list') {
        if (!$isNotification) {
            familyHubMcpWriteJsonrpc([
                'jsonrpc' => '2.0',
                'id' => $id,
                'result' => ['prompts' => []],
            ]);
        }
        return;
    }

    if ($method === 'tools/call') {
        if ($isNotification) {
            return;
        }
        $params = $msg['params'] ?? [];
        if (!is_array($params)) {
            $params = [];
        }
        $toolName = isset($params['name']) ? (string) $params['name'] : '';
        $arguments = $params['arguments'] ?? [];
        if (!is_array($arguments)) {
            $arguments = [];
        }
        $run = familyHubMcpRunTool($toolName, $arguments);
        familyHubMcpWriteJsonrpc([
            'jsonrpc' => '2.0',
            'id' => $id,
            'result' => $run,
        ]);
        return;
    }

    if ($isNotification) {
        return;
    }

    familyHubMcpWriteJsonrpc([
        'jsonrpc' => '2.0',
        'id' => $id,
        'error' => ['code' => -32601, 'message' => 'Method not found: ' . $method],
    ]);
}

if (PHP_SAPI !== 'cli') {
    fwrite(STDERR, "familyhub MCP server: run from CLI.\n");
    exit(1);
}

while (($msg = familyHubMcpReadMessage()) !== null) {
    familyHubMcpHandleMessage($msg);
}
