"use client";

import { Fragment, useState, useTransition } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import {
  ArrowDownLeft,
  ArrowUpRight,
  ChevronDown,
  ChevronLeft,
  ChevronRight,
  Copy,
  Download,
  RefreshCw,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/shared/panel";
import { StatusBadge } from "@/components/shared/status-badge";
import { useQueryUpdate } from "@/components/shared/query-controls";
import { sessionLogPageSizes, readSessionLogPageSize } from "./api-contract";
import type { OcppLog } from "@/features/chargepoints/logs/mock-data";
import { sessionDate, sessionStatusLabel } from "./presentation";

const commonActions = [
  "Authorize",
  "StartTransaction",
  "StopTransaction",
  "MeterValues",
  "StatusNotification",
  "Heartbeat",
  "TransactionEvent",
  "RemoteStartTransaction",
  "RemoteStopTransaction",
];

const timestampFormat = new Intl.DateTimeFormat("en-GB", {
  timeZone: "Asia/Jakarta",
  day: "2-digit",
  month: "short",
  year: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  second: "2-digit",
  fractionalSecondDigits: 3,
});

type LogContext = "session" | "chargepoint";

function messageTypeLabel(type?: string) {
  return ({ call: "Request", callresult: "Response", callerror: "Error" } as Record<string, string>)[type?.toLowerCase() ?? ""] ?? type ?? "Exchange";
}

function payloadFor(row: OcppLog) {
  if (!row.messageType) return { request: row.request, response: row.response };
  return row.messageType.toLowerCase() === "call" ? row.request : row.response;
}

function statusTone(status?: string) {
  if (["accepted", "success"].includes(status?.toLowerCase() ?? "")) return "success";
  if (["rejected", "failed", "error", "timeout"].includes(status?.toLowerCase() ?? "")) return "danger";
  return "neutral";
}

function timestampValue(row: OcppLog) {
  const value = Date.parse(row.timestamp);
  return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
}

function readLogPage(value: string | null) {
  const page = Number(value);
  return Number.isSafeInteger(page) && page > 0 ? page : 1;
}

function createJsonDownload(logs: OcppLog[], filename: string) {
  const url = URL.createObjectURL(
    new Blob([JSON.stringify(logs, null, 2)], { type: "application/json" }),
  );
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  link.click();
  URL.revokeObjectURL(url);
}

function responseError(payload: unknown) {
  return typeof payload === "object" && payload !== null && "error" in payload &&
    typeof payload.error === "string"
    ? payload.error
    : "Unable to prepare the log download. Please try again.";
}

export function SessionLogsView({
  data,
  sessionId,
  sessionWindow,
  context = "session",
  source = "api",
  loadError,
  loadNotice,
}: {
  data: OcppLog[];
  sessionId?: string;
  sessionWindow?: { start: string; end: string };
  context?: LogContext;
  source?: "api" | "preview";
  loadError?: string;
  loadNotice?: string;
}) {
  const router = useRouter();
  const params = useSearchParams();
  const update = useQueryUpdate();
  const [expandedId, setExpandedId] = useState<string | null>(null);
  const [copyNotice, setCopyNotice] = useState("");
  const [downloadError, setDownloadError] = useState("");
  const [downloading, setDownloading] = useState(false);
  const [refreshAttempted, setRefreshAttempted] = useState(false);
  const [isRefreshing, startRefresh] = useTransition();
  const isSession = context === "session";
  const action = params.get("log_action") ?? "";
  const direction = params.get("log_direction") ?? "";
  const status = params.get("log_status") ?? "";
  const limit = readSessionLogPageSize(params.get("log_limit") ?? undefined);
  const filteredRows = data
    .filter((row) =>
      (!action || row.action === action) &&
      (isSession ? !direction || row.direction === direction : true) &&
      (isSession ? !status || row.status === status : true),
    )
    .toSorted((left, right) => timestampValue(right) - timestampValue(left));
  const pageCount = Math.max(1, Math.ceil(filteredRows.length / limit));
  const requestedPage = readLogPage(params.get("log_page"));
  const currentPage = Math.min(requestedPage, pageCount);
  const rows = filteredRows.slice((currentPage - 1) * limit, currentPage * limit);
  const actions = Array.from(
    new Set([...commonActions, ...data.map((row) => row.action), action].filter(Boolean)),
  ).sort();
  const statuses = Array.from(
    new Set(["received", "accepted", ...data.map((row) => row.status ?? ""), status].filter(Boolean)),
  ).sort();

  function changeFilters(values: Record<string, string | null>) {
    setExpandedId(null);
    setCopyNotice("");
    setDownloadError("");
    setRefreshAttempted(false);
    update({ log_page: "1", ...values });
  }

  function refreshLogs() {
    setRefreshAttempted(true);
    startRefresh(() => router.refresh());
  }

  async function download() {
    setDownloadError("");
    setDownloading(true);
    try {
      let exportRows: OcppLog[];
      if (isSession && source === "api") {
        if (!sessionId) throw new Error("Session ID is missing; refresh the page and try again.");
        const response = await fetch(
          `/api/sessions/${encodeURIComponent(sessionId)}/log-export`,
          { headers: { Accept: "application/json" }, cache: "no-store" },
        );
        const payload: unknown = await response.json().catch(() => null);
        if (!response.ok) throw new Error(responseError(payload));
        if (typeof payload !== "object" || payload === null || !("logs" in payload) || !Array.isArray(payload.logs)) {
          throw new Error("The log service returned an unsupported download.");
        }
        exportRows = payload.logs as OcppLog[];
      } else {
        exportRows = isSession ? data : rows;
      }
      createJsonDownload(
        exportRows,
        isSession ? "session-ocpp-logs.json" : "chargepoint-ocpp-logs-page.json",
      );
    } catch (error) {
      setDownloadError(
        error instanceof Error && error.message
          ? error.message
          : "Unable to prepare the log download. Please try again.",
      );
    } finally {
      setDownloading(false);
    }
  }

  return (
    <section className="panel overflow-hidden" aria-label={`${isSession ? "Session" : "Chargepoint"} OCPP logs`} aria-busy={isRefreshing}>
      <div className="space-y-4 p-5">
        <div className="flex flex-wrap items-start justify-between gap-3">
          <div>
            <h2 className="text-base font-semibold">OCPP Logs</h2>
            <p className="mt-1 text-sm text-muted-foreground">
              {isSession
                ? "Messages associated with this session. Times are shown in WIB (UTC+7)."
                : "Messages sent to and from this chargepoint. Times are shown in WIB (UTC+7)."}
            </p>
          </div>
          <div className="flex gap-2">
            <Button variant="outline" size="sm" disabled={isRefreshing} onClick={refreshLogs}>
              <RefreshCw className={isRefreshing ? "animate-spin motion-reduce:animate-none" : undefined} aria-hidden="true" />
              {isRefreshing ? "Refreshing…" : "Refresh"}
            </Button>
            <Button
              variant="outline"
              size="sm"
              disabled={downloading || (!isSession && !rows.length) || (isSession && source === "preview" && !data.length)}
              onClick={download}
            >
              <Download aria-hidden="true" />
              {downloading ? "Preparing…" : isSession ? "Download all logs" : "Download current page"}
            </Button>
          </div>
        </div>
        {isSession && sessionWindow && (
          <p className="rounded-lg border bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
            Charging window: {sessionDate(sessionWindow.start)} → {sessionWindow.end ? sessionDate(sessionWindow.end) : "In progress"}
          </p>
        )}
        <div className={`grid items-end gap-3 ${isSession ? "sm:grid-cols-2 xl:grid-cols-[2fr_1fr_1fr_auto]" : "sm:grid-cols-[minmax(240px,2fr)_auto]"}`}>
          <label className="space-y-1.5 text-sm">
            <span className="block font-medium">Action</span>
            <select className="h-10 w-full rounded-lg border bg-card px-3" value={action} onChange={(event) => changeFilters({ log_action: event.target.value })}>
              <option value="">All actions</option>
              {actions.map((value) => <option key={value} value={value}>{value}</option>)}
            </select>
          </label>
          {isSession && <>
            <label className="space-y-1.5 text-sm">
              <span className="block font-medium">Direction</span>
              <select className="h-10 w-full rounded-lg border bg-card px-3" value={direction} onChange={(event) => changeFilters({ log_direction: event.target.value })}>
                <option value="">Both directions</option>
                <option value="inbound">Charger → CSMS</option>
                <option value="outbound">CSMS → Charger</option>
                {direction && !["inbound", "outbound"].includes(direction) && <option value={direction}>{direction}</option>}
              </select>
            </label>
            <label className="space-y-1.5 text-sm">
              <span className="block font-medium">Status</span>
              <select className="h-10 w-full rounded-lg border bg-card px-3" value={status} onChange={(event) => changeFilters({ log_status: event.target.value })}>
                <option value="">All statuses</option>
                {statuses.map((value) => <option key={value} value={value}>{sessionStatusLabel(value)}</option>)}
              </select>
            </label>
          </>}
          <Button
            variant="ghost"
            disabled={isSession ? !action && !direction && !status : !action}
            onClick={() => changeFilters(isSession
              ? { log_action: null, log_direction: null, log_status: null }
              : { log_action: null })}
          >
            Reset filters
          </Button>
        </div>
        {isRefreshing && <p role="status" className="text-sm text-muted-foreground">Refreshing {isSession ? "session" : "chargepoint"} logs…</p>}
        {!isRefreshing && refreshAttempted && loadError && (
          <p role="alert" className="text-sm text-destructive">Refresh failed. {loadError}</p>
        )}
        {!isRefreshing && refreshAttempted && !loadError && (
          <p role="status" className="text-sm text-success">{isSession ? "Session" : "Chargepoint"} logs refreshed successfully.</p>
        )}
        {loadError && !refreshAttempted && <p role="alert" className="text-sm text-destructive">{loadError}</p>}
        {loadNotice && <p role="status" className="text-sm text-muted-foreground">{loadNotice}</p>}
        {downloadError && <p role="alert" className="text-sm text-destructive">{downloadError}</p>}
        {copyNotice && <p role="status" className="text-sm text-muted-foreground">{copyNotice}</p>}
      </div>
      <div className="overflow-x-auto">
        <table className="data-table min-w-[900px]">
          <caption className="sr-only">OCPP messages sorted newest first; expand a row to inspect its payload.</caption>
          <thead>
            <tr>{["Timestamp (WIB)", "Action / Type", "Direction", "Status", "Latency", "Details"].map((label) => <th key={label}>{label}</th>)}</tr>
          </thead>
          <tbody>
            {rows.map((row) => {
              const expanded = expandedId === row.id;
              const inbound = row.direction === "inbound";
              const outbound = row.direction === "outbound";
              return (
                <Fragment key={row.id}>
                  <tr>
                    <td className="whitespace-nowrap text-xs tabular-nums">
                      {Number.isFinite(Date.parse(row.timestamp)) ? timestampFormat.format(new Date(row.timestamp)) : "—"}
                    </td>
                    <td>
                      <p className="font-medium">{row.action}</p>
                      <p className="mt-1 text-xs text-muted-foreground">{messageTypeLabel(row.messageType)}</p>
                    </td>
                    <td>
                      <span className="inline-flex items-center gap-1.5 whitespace-nowrap text-xs text-muted-foreground">
                        {inbound ? <ArrowDownLeft className="size-4 text-info" aria-hidden="true" /> : outbound ? <ArrowUpRight className="size-4" aria-hidden="true" /> : null}
                        {inbound ? "Charger → CSMS" : outbound ? "CSMS → Charger" : row.direction || row.origin || "—"}
                      </span>
                    </td>
                    <td><StatusBadge tone={statusTone(row.status)}>{row.status ? sessionStatusLabel(row.status) : "—"}</StatusBadge></td>
                    <td className="whitespace-nowrap text-xs tabular-nums">{row.latencyMs !== undefined ? `${row.latencyMs} ms` : "—"}</td>
                    <td>
                      <Button
                        variant="ghost"
                        size="sm"
                        aria-expanded={expanded}
                        aria-controls={`log-${row.id}`}
                        aria-label={`${expanded ? "Hide" : "View"} log ${row.id}`}
                        onClick={() => setExpandedId(expanded ? null : row.id)}
                      >
                        Payload<ChevronDown className={`size-4 ${expanded ? "rotate-180" : ""}`} aria-hidden="true" />
                      </Button>
                    </td>
                  </tr>
                  {expanded && (
                    <tr id={`log-${row.id}`}>
                      <td colSpan={6} className="bg-muted/30">
                        <div className="space-y-4 p-2">
                          <dl className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
                            {[
                              ["Message ID", row.messageId ?? row.id],
                              ["Record ID", row.id],
                              ["Chargepoint", row.chargepoint],
                              ["Protocol", row.protocol || "—"],
                              ["Trace ID", row.traceId || "—"],
                            ].map(([label, value]) => (
                              <div key={label}>
                                <dt className="metric-label">{label}</dt>
                                <dd className="mt-1 break-all font-mono text-xs">{value}</dd>
                              </div>
                            ))}
                          </dl>
                          <div className="rounded-lg border bg-card p-4">
                            <div className="mb-3 flex items-center justify-between gap-3">
                              <h3 className="text-sm font-semibold">{row.messageType ? `${messageTypeLabel(row.messageType)} payload` : "Request and response"}</h3>
                              <Button
                                variant="outline"
                                size="sm"
                                onClick={async () => {
                                  try {
                                    await navigator.clipboard.writeText(JSON.stringify(payloadFor(row), null, 2));
                                    setCopyNotice("Payload copied.");
                                  } catch {
                                    setCopyNotice("Unable to copy. Select the JSON and copy it manually.");
                                  }
                                }}
                              >
                                <Copy aria-hidden="true" />Copy JSON
                              </Button>
                            </div>
                            <pre tabIndex={0} aria-label={`Payload for ${row.action}`} className="max-h-80 overflow-auto rounded-lg bg-muted/50 p-3 font-mono text-xs">
                              {JSON.stringify(payloadFor(row), null, 2)}
                            </pre>
                            {row.rawFrame && (
                              <details className="mt-3 text-xs text-muted-foreground">
                                <summary className="cursor-pointer rounded py-2 focus-visible:outline-2 focus-visible:outline-ring">Original OCPP frame</summary>
                                <pre tabIndex={0} className="max-h-64 overflow-auto rounded-lg bg-muted/50 p-3 font-mono">{JSON.stringify(row.rawFrame, null, 2)}</pre>
                              </details>
                            )}
                          </div>
                        </div>
                      </td>
                    </tr>
                  )}
                </Fragment>
              );
            })}
          </tbody>
        </table>
        {!rows.length && <EmptyState title={loadError ? "Logs unavailable" : "No matching logs"} />}
      </div>
      <div className="flex flex-wrap items-center justify-between gap-3 border-t px-5 py-4 text-sm">
        <p className="text-muted-foreground">
          Showing {rows.length ? (currentPage - 1) * limit + 1 : 0}–{(currentPage - 1) * limit + rows.length} of {filteredRows.length} {isSession ? "messages" : "loaded messages"}
        </p>
        <div className="flex flex-wrap items-center gap-3">
          {pageCount > 1 && (
            <div className="flex items-center gap-2">
              <span className="text-muted-foreground">Page {currentPage} of {pageCount}</span>
              <Button
                variant="outline"
                size="sm"
                aria-label="Previous logs page"
                disabled={currentPage <= 1}
                onClick={() => changeFilters({ log_page: String(currentPage - 1) })}
              >
                <ChevronLeft aria-hidden="true" />Previous
              </Button>
              <Button
                variant="outline"
                size="sm"
                aria-label="Next logs page"
                disabled={currentPage >= pageCount}
                onClick={() => changeFilters({ log_page: String(currentPage + 1) })}
              >
                Next<ChevronRight aria-hidden="true" />
              </Button>
            </div>
          )}
          <label className="flex items-center gap-2">
            Rows per page
            <select
              className="h-9 rounded-lg border bg-card px-3"
              value={limit}
              onChange={(event) => changeFilters({ log_limit: event.target.value })}
            >
              {sessionLogPageSizes.map((value) => <option key={value} value={value}>{value}</option>)}
            </select>
          </label>
        </div>
      </div>
    </section>
  );
}
