<?php
namespace App\Controllers;

use App\Core\Controller;
use App\Core\Database;
use App\Core\PortalAuth;
use App\Core\Request;
use App\Core\Session;
use App\Services\AttachmentService;
use App\Services\DeflectionSearch;
use App\Services\TicketService;

class PortalController extends Controller
{
    public function __construct()
    {
        PortalAuth::boot();
    }

    private function contact(): array { return PortalAuth::contact(); }

    /** Visibility scope per FR-SUP-063: primary contact sees the whole org, others see only their own. */
    private function ticketScopeSql(): array
    {
        $contact = $this->contact();
        if ($contact['is_primary']) {
            return ['t.customer_id = :cust', ['cust' => $contact['customer_id']]];
        }
        return ['t.contact_id = :contact', ['contact' => $contact['id']]];
    }

    public function dashboard(): void
    {
        [$scopeSql, $scopeParams] = $this->ticketScopeSql();

        $openCount = (int)Database::scalar("SELECT COUNT(*) FROM tickets t WHERE {$scopeSql} AND t.status NOT IN ('CLOSED','REJECTED')", $scopeParams);
        $awaitingReplyCount = (int)Database::scalar("SELECT COUNT(*) FROM tickets t WHERE {$scopeSql} AND t.status = 'PENDING_CUSTOMER'", $scopeParams);
        $resolvedThisMonth = (int)Database::scalar(
            "SELECT COUNT(*) FROM tickets t WHERE {$scopeSql} AND t.status IN ('RESOLVED','CLOSED') AND t.resolved_at >= DATE_FORMAT(NOW(), '%Y-%m-01')",
            $scopeParams
        );
        $totalCount = (int)Database::scalar("SELECT COUNT(*) FROM tickets t WHERE {$scopeSql}", $scopeParams);

        $recentTickets = Database::all(
            "SELECT t.* FROM tickets t WHERE {$scopeSql} ORDER BY t.created_at DESC LIMIT 5", $scopeParams
        );

        $byStatus = Database::all(
            "SELECT t.status, COUNT(*) AS c FROM tickets t WHERE {$scopeSql} GROUP BY t.status ORDER BY c DESC",
            $scopeParams
        );

        $this->render('portal/dashboard', [
            'openCount' => $openCount, 'awaitingReplyCount' => $awaitingReplyCount,
            'resolvedThisMonth' => $resolvedThisMonth, 'totalCount' => $totalCount,
            'recentTickets' => $recentTickets, 'byStatus' => $byStatus,
        ], 'portal/layout/portal');
    }

    /**
     * Drill-down target for the dashboard's stat tiles and status bars. `status` accepts a
     * real ENUM value for an exact match, or one of two virtual filters that mirror exactly
     * what the dashboard tile counted: OPEN (every non-closed/rejected status) and
     * RESOLVED_MONTH (resolved or closed, within the current calendar month).
     */
    public function tickets(): void
    {
        [$scopeSql, $scopeParams] = $this->ticketScopeSql();
        $status = Request::query('status', '');
        $where = $scopeSql;

        if ($status === 'OPEN') {
            $where .= " AND t.status NOT IN ('CLOSED','REJECTED')";
        } elseif ($status === 'RESOLVED_MONTH') {
            $where .= " AND t.status IN ('RESOLVED','CLOSED') AND t.resolved_at >= DATE_FORMAT(NOW(), '%Y-%m-01')";
        } elseif ($status !== '') {
            $where .= ' AND t.status = :status';
            $scopeParams['status'] = $status;
        }

        $tickets = Database::all(
            "SELECT t.*, p.code AS p_code, p.color AS p_color FROM tickets t JOIN priorities p ON p.id = t.priority_id
             WHERE {$where} ORDER BY t.created_at DESC", $scopeParams
        );
        $this->render('portal/tickets', ['tickets' => $tickets, 'status' => $status], 'portal/layout/portal');
    }

    public function createForm(): void
    {
        $contact = $this->contact();
        $products = Database::all(
            "SELECT DISTINCT p.* FROM products p
             JOIN contract_products cp ON cp.product_id = p.id
             JOIN contracts c ON c.id = cp.contract_id
             WHERE c.customer_id = :c AND c.is_active = 1 AND CURDATE() BETWEEN c.start_date AND c.end_date",
            ['c' => $contact['customer_id']]
        );
        if (empty($products)) {
            $products = Database::all('SELECT * FROM products WHERE is_active = 1');
        }
        $categories = Database::all('SELECT * FROM categories WHERE is_active = 1 ORDER BY parent_id IS NULL DESC, name');
        $priorities = Database::all('SELECT * FROM priorities WHERE is_active = 1 ORDER BY sort_order');

        // Real "before you submit" check: is this already being tracked? Helps avoid
        // duplicate tickets for the same issue (BRD's duplicate-detection intent, FR-SUP-008).
        [$scopeSql, $scopeParams] = $this->ticketScopeSql();
        $openTickets = Database::all(
            "SELECT t.id, t.ticket_no, t.subject, t.status FROM tickets t
             WHERE {$scopeSql} AND t.status NOT IN ('CLOSED','REJECTED')
             ORDER BY t.created_at DESC LIMIT 6",
            $scopeParams
        );

        $this->render('portal/create', compact('products', 'categories', 'priorities', 'openTickets'), 'portal/layout/portal');
    }

    public function store(): void
    {
        $this->requireCsrf();
        try {
            $ticket = TicketService::create([
                'contact_id' => $this->contact()['id'],
                'product_id' => (int)Request::post('product_id'),
                'subject' => trim((string)Request::post('subject')),
                'description' => trim((string)Request::post('description')),
                'category_id' => (int)Request::post('category_id'),
                'priority_id' => (int)Request::post('priority_id'),
                'severity' => Request::post('severity', 'Minor impact'),
                'channel' => 'Portal',
            ], null, $this->contact()['id']);

            $this->attachFilesToMessage('attachments', $ticket['initial_message_id'], (int)$ticket['id'], null, $this->contact()['id']);

            Session::flash('success', "Ticket {$ticket['ticket_no']} submitted. We'll be in touch within the promised response time.");
            $this->redirect('/portal/tickets/' . $ticket['id']);
        } catch (\Throwable $e) {
            Session::flash('error', $e->getMessage());
            $this->redirect('/portal/tickets/new');
        }
    }

    public function show(string $id): void
    {
        [$scopeSql, $scopeParams] = $this->ticketScopeSql();
        $scopeParams['id'] = $id;
        $ticket = Database::one("SELECT t.* FROM tickets t WHERE t.id = :id AND {$scopeSql}", $scopeParams);
        if (!$ticket) { $this->notFound(); return; }

        // FR-SUP-070: only public messages, never internal notes.
        $messages = Database::all(
            "SELECT m.*, u.name AS user_name, ct.name AS contact_name FROM ticket_messages m
             LEFT JOIN users u ON u.id = m.user_id LEFT JOIN contacts ct ON ct.id = m.contact_id
             WHERE m.ticket_id = :t AND m.type IN ('public_reply','inbound_email') ORDER BY m.created_at ASC",
            ['t' => $id]
        );
        $attachments = Database::all('SELECT * FROM ticket_attachments WHERE ticket_id = :t', ['t' => $id]);
        $attachmentsByMsg = [];
        foreach ($attachments as $a) { $attachmentsByMsg[$a['ticket_message_id'] ?? 0][] = $a; }

        // The opening message (the ticket description) is rendered separately from the
        // reply thread — pull it off here rather than filtering the loop by timestamp,
        // so its attachments are addressable by id instead of silently dropped.
        $openingMessage = array_shift($messages);

        $priority = Database::one('SELECT * FROM priorities WHERE id = :id', ['id' => $ticket['priority_id']]);
        $csat = Database::one('SELECT * FROM csat_responses WHERE ticket_id = :id', ['id' => $id]);

        $this->render('portal/show', compact('ticket', 'messages', 'openingMessage', 'attachmentsByMsg', 'priority', 'csat'), 'portal/layout/portal');
    }

    /**
     * FR-SUP-065 / FR-SUP-070: a portal user may download an attachment only if it
     * belongs to a ticket in their visibility scope AND is attached to a public
     * message. Attachments added by an agent outside the conversation (ticket_message_id
     * NULL, e.g. the console's standalone "add attachment" action) are not exposed here.
     */
    public function downloadAttachment(string $id, string $attId): void
    {
        $ticket = $this->ownedTicketOr404($id);
        $att = Database::one(
            "SELECT a.* FROM ticket_attachments a
             JOIN ticket_messages m ON m.id = a.ticket_message_id
             WHERE a.id = :att AND a.ticket_id = :t AND m.type IN ('public_reply','inbound_email')",
            ['att' => $attId, 't' => $ticket['id']]
        );
        if (!$att) { $this->notFound(); return; }
        AttachmentService::streamDownload($att);
    }

    public function reply(string $id): void
    {
        $this->requireCsrf();
        $ticket = $this->ownedTicketOr404($id);
        $body = trim((string)Request::post('body'));
        if ($body !== '') {
            try {
                $messageId = TicketService::addMessage($ticket, 'public_reply', $body, null, $this->contact()['id']);
                $this->attachFilesToMessage('attachments', $messageId, (int)$ticket['id'], null, $this->contact()['id']);
            } catch (\Throwable $e) {
                Session::flash('error', $e->getMessage());
            }
        }
        $this->redirect('/portal/tickets/' . $id);
    }

    public function accept(string $id): void
    {
        $this->requireCsrf();
        $ticket = $this->ownedTicketOr404($id);
        if ($ticket['status'] === 'RESOLVED') {
            TicketService::changeStatus($ticket, 'CLOSED', null, 'Customer accepted the resolution');
            Session::flash('success', 'Thank you — the ticket is now closed.');
        }
        $this->redirect('/portal/tickets/' . $id);
    }

    public function reopen(string $id): void
    {
        $this->requireCsrf();
        $ticket = $this->ownedTicketOr404($id);
        $reason = trim((string)Request::post('reason', 'Issue still occurring'));
        if (in_array($ticket['status'], ['RESOLVED', 'CLOSED'], true)) {
            TicketService::changeStatus($ticket, 'REOPENED', null, $reason);
            Database::update('tickets', ['reopen_count' => (int)$ticket['reopen_count'] + 1], 'id = :id', ['id' => $id]);
            Session::flash('success', 'Ticket reopened — an agent will follow up.');
        }
        $this->redirect('/portal/tickets/' . $id);
    }

    public function csat(string $id): void
    {
        $this->requireCsrf();
        $ticket = $this->ownedTicketOr404($id);
        if ($ticket['status'] !== 'CLOSED') { $this->redirect('/portal/tickets/' . $id); return; }
        try {
            Database::insert('csat_responses', [
                'ticket_id' => $id, 'rating' => (int)Request::post('rating'), 'comment' => Request::post('comment'),
            ]);
            Session::flash('success', 'Thanks for the feedback!');
        } catch (\Throwable $e) {
            Session::flash('info', 'You already rated this ticket.');
        }
        $this->redirect('/portal/tickets/' . $id);
    }

    /** FR-SUP-057: a requester who resolves their own problem via a suggested article, instead of submitting. */
    public function deflect(): void
    {
        $this->requireCsrf();
        $articleId = (int)Request::post('article_id');
        if ($articleId) {
            Database::insert('kb_article_usage', ['kb_article_id' => $articleId, 'action' => 'deflected']);
        }
        Session::flash('success', "Glad that helped! You didn't need to raise a ticket.");
        $this->redirect('/portal');
    }

    public function kbShow(string $slug): void
    {
        $article = Database::one("SELECT * FROM kb_articles WHERE slug = :s AND status='published' AND visibility='customer'", ['s' => $slug]);
        if (!$article) { $this->notFound(); return; }
        Database::run('UPDATE kb_articles SET views = views + 1 WHERE id = :id', ['id' => $article['id']]);
        $this->render('portal/kb_show', compact('article'), 'portal/layout/portal');
    }

    /** AJAX deflection search used by the "Raise a ticket" form (FR-SUP-056) — public within the portal session. */
    public function kbSearch(): void
    {
        PortalAuth::boot();
        $q = trim((string)Request::query('q', ''));
        $results = DeflectionSearch::suggest($q, 'customer', 3);
        foreach ($results as $r) {
            Database::insert('kb_article_usage', ['kb_article_id' => $r['id'], 'action' => 'suggested']);
        }
        $results = array_map(fn($r) => [
            'id' => $r['id'], 'title' => $r['title'], 'excerpt' => strip_tags($r['excerpt']),
            'url' => \App\Core\View::url('/portal/kb/' . ($r['slug'] ?? '')),
        ], $results);
        $this->json($results);
    }

    private function ownedTicketOr404(string $id): array
    {
        [$scopeSql, $scopeParams] = $this->ticketScopeSql();
        $scopeParams['id'] = $id;
        $ticket = Database::one("SELECT t.* FROM tickets t WHERE t.id = :id AND {$scopeSql}", $scopeParams);
        if (!$ticket) { $this->notFound(); }
        return $ticket;
    }

    /** Stores every uploaded file under $fieldName and links each to a specific ticket message. */
    private function attachFilesToMessage(string $fieldName, ?int $messageId, int $ticketId, ?int $userId, ?int $contactId): void
    {
        foreach (Request::files($fieldName) as $file) {
            $meta = AttachmentService::store($file, $ticketId);
            Database::insert('ticket_attachments', array_merge($meta, [
                'ticket_id' => $ticketId, 'ticket_message_id' => $messageId,
                'uploaded_by_user_id' => $userId, 'uploaded_by_contact_id' => $contactId,
            ]));
        }
    }
}
