<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Core\App;
use App\Core\Availability;
use App\Core\Database as DB;
use App\Core\RateLimiter;
use App\Core\Request;
use App\Core\Response;
use App\Core\Schema;

/** Public read API consumed by the React frontend, plus the contact form POST. */
final class ApiController
{
    /** Settings that must never leave the server via the public API. */
    private const SECRET_SETTINGS = [
        'ai_api_key',
        'paypal_secret',
        'wipay_account_number',
        'wipay_api_key',
    ];

    public function settings(): never
    {
        $rows = DB::select('SELECT name, value FROM settings');
        $raw = [];
        $out = [];
        foreach ($rows as $row) {
            $raw[$row['name']] = (string) $row['value'];
            if (in_array($row['name'], self::SECRET_SETTINGS, true)) {
                continue;
            }
            $decoded = json_decode($row['value'], true);
            $out[$row['name']] = $decoded ?? $row['value'];
        }

        // Payment providers only look enabled to the frontend when they are
        // fully configured server-side (keys never leave the server).
        $out['paypal_enabled'] = ($raw['paypal_enabled'] ?? '') === '1'
            && trim($raw['paypal_client_id'] ?? '') !== ''
            && trim($raw['paypal_secret'] ?? '') !== '' ? '1' : '0';
        $out['wipay_enabled'] = ($raw['wipay_enabled'] ?? '') === '1'
            && (($raw['wipay_sandbox'] ?? '') === '1'
                || (trim($raw['wipay_account_number'] ?? '') !== ''
                    && trim($raw['wipay_api_key'] ?? '') !== '')) ? '1' : '0';

        Response::json($out);
    }

    public function homepage(): never
    {
        $rows = DB::select('SELECT section, content FROM homepage_sections WHERE active = 1');
        $out = [];
        foreach ($rows as $row) {
            $out[$row['section']] = json_decode($row['content'], true);
        }
        Response::json($out);
    }

    public function treatments(): never
    {
        $rows = DB::select(
            'SELECT t.id, t.slug, t.name, t.description, t.duration, t.price, t.image,
                    t.featured, c.name AS category
             FROM treatments t
             LEFT JOIN treatment_categories c ON c.id = t.category_id
             WHERE t.status = 1 ORDER BY t.sort_order, t.id',
        );
        Response::json(array_map(fn($r) => [
            ...$r,
            'price'    => (float) $r['price'],
            'featured' => (bool) $r['featured'],
            'category' => $r['category'] ?? 'Massage',
        ], $rows));
    }

    public function rooms(): never
    {
        Schema::heal();
        $rows = DB::select('SELECT id, slug, name, description, image FROM rooms WHERE active = 1 ORDER BY sort_order, id');
        Response::json(array_map(fn($r) => [...$r, 'id' => (int) $r['id']], $rows));
    }

    /** Open booking slots for a date (?date=YYYY-MM-DD&room_id=optional), grouped by room. */
    public function availability(Request $request): never
    {
        $date = $request->query('date');
        $roomId = $request->query('room_id');
        Response::json(Availability::forDate($date, $roomId === '' ? null : (int) $roomId));
    }

    public function packages(): never
    {
        $rows = DB::select(
            'SELECT id, slug, name, tag, dates_label, description, included, price,
                    price_label, promo_price, promo_label, image
             FROM packages WHERE active = 1 ORDER BY sort_order, id',
        );
        Response::json(array_map(fn($r) => [
            ...$r,
            'included'    => json_decode($r['included'] ?: '[]', true),
            'price'       => (float) $r['price'],
            'promo_price' => $r['promo_price'] !== null ? (float) $r['promo_price'] : null,
        ], $rows));
    }

    public function gallery(): never
    {
        $rows = DB::select(
            'SELECT i.id, i.src, i.alt, i.size, g.name AS category
             FROM gallery_images i
             LEFT JOIN gallery g ON g.id = i.gallery_id
             WHERE i.active = 1 ORDER BY i.sort_order, i.id',
        );
        Response::json(array_map(fn($r) => [...$r, 'category' => $r['category'] ?? 'Spa'], $rows));
    }

    public function testimonials(): never
    {
        Response::json(DB::select(
            'SELECT id, client_name, location, review, rating, photo, date
             FROM testimonials WHERE approved = 1 ORDER BY sort_order, date DESC',
        ));
    }

    public function team(): never
    {
        $rows = DB::select(
            'SELECT id, name, position, bio, photo, socials FROM team_members
             WHERE active = 1 ORDER BY sort_order, id',
        );
        Response::json(array_map(fn($r) => [
            ...$r,
            'socials' => json_decode($r['socials'] ?: '{}', true),
        ], $rows));
    }

    public function blog(): never
    {
        $rows = DB::select(
            "SELECT b.id, b.slug, b.title, b.excerpt, b.body, b.image, b.published_at, b.tags,
                    c.name AS category
             FROM blogs b
             LEFT JOIN blog_categories c ON c.id = b.category_id
             WHERE b.status = 'published' AND b.published_at <= ?
             ORDER BY b.published_at DESC",
            [date('Y-m-d H:i:s')],
        );
        Response::json(array_map(fn($r) => [
            ...$r,
            'category' => $r['category'] ?? 'Wellness',
            'tags'     => json_decode($r['tags'] ?: '[]', true),
        ], $rows));
    }

    public function faqs(): never
    {
        Response::json(DB::select(
            'SELECT id, question, answer FROM faqs WHERE active = 1 ORDER BY sort_order, id',
        ));
    }

    /**
     * Active pages for the site menu (slug + title only). Privacy and terms
     * stay footer-only, so they are excluded here.
     */
    public function pages(): never
    {
        Response::json(DB::select(
            "SELECT slug, title FROM pages
             WHERE active = 1 AND slug NOT IN ('privacy', 'terms') ORDER BY id",
        ));
    }

    /** @param array<string, string> $params */
    public function page(Request $request, array $params): never
    {
        Schema::heal(); // image/subtitle columns exist on pre-existing installs
        $page = DB::selectOne(
            'SELECT slug, title, body, image, subtitle FROM pages WHERE slug = ? AND active = 1',
            [$params['slug']],
        );
        $page === null ? Response::error('Not found', 404) : Response::json($page);
    }

    public function contact(Request $request): never
    {
        $cfg = App::config('security')['rate_limit'];
        if (!RateLimiter::check('contact:' . $request->ip(), $cfg['max'], $cfg['window'])) {
            Response::error('Too many requests — please try again later.', 429);
        }

        $name = $request->str('name');
        $email = $request->str('email');
        $message = $request->str('message');
        $phone = $request->str('phone');

        if (mb_strlen($name) < 2 || mb_strlen($name) > 120) {
            Response::error('Please provide your name.');
        }
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            Response::error('Please provide a valid email address.');
        }
        if (mb_strlen($message) < 10 || mb_strlen($message) > 5000) {
            Response::error('Please provide a message (10–5000 characters).');
        }

        DB::insert('contacts', [
            'name'       => $name,
            'email'      => $email,
            'phone'      => mb_substr($phone, 0, 40),
            'message'    => $message,
            'ip'         => $request->ip(),
            'created_at' => date('Y-m-d H:i:s'),
        ]);

        $mail = App::config('mail');
        if (!empty($mail['host'])) {
            // Fire-and-forget notification; failures never block the response.
            @mail(
                $mail['to'],
                'New enquiry from ' . $name,
                "From: $name <$email>\nPhone: $phone\n\n$message",
                'From: ' . $mail['from'],
            );
        }

        Response::json(['ok' => true], 201);
    }

    // --- AI assistant -----------------------------------------------------------

    /** Website chat assistant: proxies the conversation to the Claude API. */
    public function chat(Request $request): never
    {
        if (!RateLimiter::check('chat:' . $request->ip(), 10, 60)) {
            Response::error('Too many messages — please wait a moment.', 429);
        }

        $settings = $this->settingsMap();
        $apiKey = trim($settings['ai_api_key'] ?? '');
        if (($settings['ai_enabled'] ?? '') !== '1' || $apiKey === '') {
            Response::error('The assistant is not available right now.', 503);
        }

        // Sanitize the client-supplied history: valid roles, plain strings,
        // bounded length, and it must start with (and end on) a user turn.
        $messages = [];
        $incoming = $request->input('messages');
        foreach (array_slice(is_array($incoming) ? $incoming : [], -12) as $m) {
            $role = is_array($m) ? ($m['role'] ?? '') : '';
            $text = is_array($m) && is_string($m['content'] ?? null) ? trim($m['content']) : '';
            if (!in_array($role, ['user', 'assistant'], true) || $text === '') {
                continue;
            }
            $messages[] = ['role' => $role, 'content' => mb_substr($text, 0, 2000)];
        }
        while ($messages !== [] && $messages[0]['role'] !== 'user') {
            array_shift($messages);
        }
        if ($messages === [] || end($messages)['role'] !== 'user') {
            Response::error('Please provide a message.');
        }

        $payload = [
            'model'      => trim($settings['ai_model'] ?? '') ?: 'claude-opus-4-8',
            'max_tokens' => 700,
            'system'     => [[
                'type'          => 'text',
                'text'          => $this->assistantContext($settings),
                // Stable per-site context: let the API cache it across chats.
                'cache_control' => ['type' => 'ephemeral'],
            ]],
            'messages'   => $messages,
        ];

        $result = $this->anthropic($apiKey, $payload);
        if (($result['stop_reason'] ?? '') === 'refusal') {
            Response::json(['reply' => "I can't help with that one — but I'm happy to answer "
                . 'questions about our treatments, packages, prices, or opening hours!']);
        }
        $reply = '';
        foreach ($result['content'] ?? [] as $block) {
            if (($block['type'] ?? '') === 'text') {
                $reply .= $block['text'];
            }
        }
        if (trim($reply) === '') {
            Response::error('The assistant could not answer — please try again.', 502);
        }
        Response::json(['reply' => $reply]);
    }

    /** @return array<string, string> raw settings name => value */
    private function settingsMap(): array
    {
        $out = [];
        foreach (DB::select('SELECT name, value FROM settings') as $row) {
            $out[$row['name']] = (string) $row['value'];
        }
        return $out;
    }

    /** Builds the assistant system prompt from live site content. */
    private function assistantContext(array $settings): string
    {
        $company = $settings['company_name'] ?? 'Touch Therapies Antigua';
        $name = trim($settings['ai_name'] ?? '') ?: 'the website assistant';

        $lines = [];
        $lines[] = "You are {$name}, the friendly virtual assistant on the website of {$company},"
            . ' a massage and wellness spa. Answer briefly and warmly (2–4 sentences unless a list'
            . ' is needed). Only discuss this business — its treatments, packages, prices, hours,'
            . ' location, and booking. If asked about anything unrelated, politely steer back.'
            . ' Never invent prices or services that are not listed below. You cannot make'
            . ' bookings yourself: to book, direct guests to WhatsApp or phone.';

        $lines[] = "\n## Business details";
        foreach (['address' => 'Address', 'phone' => 'Phone', 'whatsapp' => 'WhatsApp', 'email' => 'Email'] as $key => $label) {
            if (!empty($settings[$key])) {
                $lines[] = "$label: {$settings[$key]}";
            }
        }
        foreach (json_decode($settings['hours'] ?? '[]', true) ?: [] as $h) {
            if (is_array($h)) {
                $lines[] = 'Hours ' . ($h['day'] ?? '') . ': ' . ($h['time'] ?? '');
            }
        }
        $methods = json_decode($settings['payment_methods'] ?? '[]', true);
        if (is_array($methods) && $methods !== []) {
            $lines[] = 'Payment methods accepted: ' . implode(', ', $methods);
        }

        $lines[] = "\n## Treatments (name — duration — price USD)";
        $rows = DB::select('SELECT name, duration, price, description FROM treatments WHERE status = 1 ORDER BY sort_order, id');
        foreach ($rows as $t) {
            $lines[] = "- {$t['name']} — {$t['duration']} — \${$t['price']}: " . mb_substr($t['description'], 0, 200);
        }

        $lines[] = "\n## Packages";
        $rows = DB::select('SELECT name, description, included, price, price_label, promo_price FROM packages WHERE active = 1 ORDER BY sort_order, id');
        foreach ($rows as $p) {
            $included = implode(', ', json_decode($p['included'] ?: '[]', true) ?: []);
            $price = $p['promo_price'] !== null ? "\${$p['promo_price']} (promo, normally \${$p['price']})" : "\${$p['price']}";
            $lines[] = "- {$p['name']} — {$price} {$p['price_label']}: " . mb_substr($p['description'], 0, 200)
                . ($included !== '' ? " Includes: $included." : '');
        }

        $lines[] = "\n## Frequently asked questions";
        $rows = DB::select('SELECT question, answer FROM faqs WHERE active = 1 ORDER BY sort_order, id');
        foreach ($rows as $f) {
            $lines[] = "Q: {$f['question']}\nA: " . mb_substr(strip_tags($f['answer']), 0, 400);
        }

        return implode("\n", $lines);
    }

    /**
     * Minimal Claude API client (no Composer on this stack): curl when the
     * extension exists (production), stream context otherwise (local dev).
     *
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    private function anthropic(string $apiKey, array $payload): array
    {
        $base = App::config('ai')['base_url'] ?? 'https://api.anthropic.com';
        $url = rtrim($base, '/') . '/v1/messages';
        $body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $headers = [
            'Content-Type: application/json',
            'x-api-key: ' . $apiKey,
            'anthropic-version: 2023-06-01',
        ];

        if (function_exists('curl_init')) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST           => true,
                CURLOPT_POSTFIELDS     => $body,
                CURLOPT_HTTPHEADER     => $headers,
                CURLOPT_TIMEOUT        => 60,
            ]);
            $raw = curl_exec($ch);
            $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            $err = curl_error($ch);
            curl_close($ch);
        } else {
            $ctx = stream_context_create(['http' => [
                'method'        => 'POST',
                'header'        => implode("\r\n", $headers),
                'content'       => $body,
                'timeout'       => 60,
                'ignore_errors' => true,
            ]]);
            $raw = @file_get_contents($url, false, $ctx);
            $status = 0;
            foreach ($http_response_header ?? [] as $h) {
                if (preg_match('#^HTTP/\S+\s+(\d{3})#', $h, $m)) {
                    $status = (int) $m[1];
                }
            }
            $err = $raw === false ? 'request failed' : '';
        }

        $decoded = is_string($raw) ? json_decode($raw, true) : null;
        if ($raw === false || !is_array($decoded)) {
            error_log("AI chat: transport error ($err)");
            Response::error('The assistant is temporarily unreachable.', 502);
        }
        if ($status === 429 || $status === 529) {
            Response::error('The assistant is busy — please try again shortly.', 503);
        }
        if ($status < 200 || $status >= 300) {
            error_log('AI chat: API error ' . $status . ' ' . ($decoded['error']['message'] ?? ''));
            Response::error('The assistant hit a problem — please try again later.', 502);
        }
        return $decoded;
    }
}
