import {
  Alert,
  Box,
  Button,
  Chip,
  Divider,
  MenuItem,
  Paper,
  Select,
  Stack,
  StructuredDialog,
  TextField,
  Typography
} from "@exotel-npm-dev/signal-design-system";
import { useMemo, useState, type MutableRefObject } from "react";
import type { AafClient } from "../aaf/gwtAafSdk";
import { applyContextToSample, type AafEventContext } from "../utils/eventContext";
import {
  AAF_API_NAMESPACES,
  AAF_METHODS,
  methodSubtitle,
  type AafApiNamespace,
  type AafMethodSpec
} from "../utils/methodCatalog";
import { safeJson } from "../utils/safeJson";

type MethodResult =
  | { kind: "ok"; ts: number; value: unknown }
  | { kind: "error"; ts: number; message: string };

function formatTs(ts: number): string {
  const d = new Date(ts);
  const hh = String(d.getHours()).padStart(2, "0");
  const mm = String(d.getMinutes()).padStart(2, "0");
  const ss = String(d.getSeconds()).padStart(2, "0");
  const ms = String(d.getMilliseconds()).padStart(3, "0");
  return `${hh}:${mm}:${ss}.${ms}`;
}

function tryParseJsonArgs(text: string): unknown[] | { error: string } {
  const t = text.trim();
  if (t === "") return [];
  try {
    const parsed = JSON.parse(t);
    return Array.isArray(parsed) ? parsed : [parsed];
  } catch (e) {
    return { error: e instanceof Error ? e.message : String(e) };
  }
}

/**
 * `aaf_sdk.js` calls are spread verbatim — `client[api][method](...args)`. The catalogue
 * already owns the correct argument shape, and other `globalData` methods (`update`,
 * `getTickets`, `addNote`, `bulkUpdate`, `bulkAction`, `transfer`, `invoke`) take
 * positional `(dataObject: string, …)`, so we must not coerce here.
 */
function callApiMethod(
  client: AafClient,
  api: AafApiNamespace,
  method: string,
  args: unknown[]
): Promise<unknown> {
  const apiObj = (client as unknown as Record<string, Record<string, unknown>>)[api];
  if (!apiObj || typeof apiObj !== "object") {
    return Promise.reject(
      new Error(`Namespace client.${api} is not available on this AAF SDK build.`)
    );
  }
  const fn = apiObj[method];
  if (typeof fn !== "function") {
    return Promise.reject(new Error(`Method client.${api}.${method} is not a function.`));
  }
  let out: unknown;
  try {
    out = (fn as (...a: unknown[]) => unknown).apply(apiObj, args);
  } catch (e) {
    return Promise.reject(e);
  }
  if (out && typeof (out as Promise<unknown>).then === "function") return out as Promise<unknown>;
  return Promise.resolve(out);
}

function MethodCard({
  method,
  ctx,
  selected,
  onToggle,
  clientRef
}: {
  method: AafMethodSpec;
  ctx: AafEventContext;
  selected: boolean;
  onToggle: () => void;
  clientRef: MutableRefObject<AafClient | null>;
}) {
  const initial = useMemo(() => applyContextToSample(method.sampleRequest, ctx), [method.sampleRequest, ctx]);
  const [text, setText] = useState(initial);
  const [busy, setBusy] = useState(false);
  const [result, setResult] = useState<MethodResult | null>(null);

  const onRun = () => {
    const client = clientRef.current;
    if (!client) {
      setResult({ kind: "error", ts: Date.now(), message: "AmeyoClient not ready yet." });
      return;
    }
    const parsed = tryParseJsonArgs(text);
    if (!Array.isArray(parsed)) {
      setResult({ kind: "error", ts: Date.now(), message: `Invalid JSON: ${parsed.error}` });
      return;
    }
    setBusy(true);
    setResult(null);
    callApiMethod(client, method.api, method.name, parsed)
      .then((value) => setResult({ kind: "ok", ts: Date.now(), value }))
      .catch((err: unknown) =>
        setResult({
          kind: "error",
          ts: Date.now(),
          message: err instanceof Error ? err.message : safeJson(err)
        })
      )
      .finally(() => setBusy(false));
  };

  return (
    <Paper variant="outlined" sx={{ overflow: "hidden" }}>
      <Box
        onClick={onToggle}
        sx={{
          px: 1.5,
          py: 1,
          display: "flex",
          alignItems: "flex-start",
          gap: 1,
          cursor: "pointer",
          bgcolor: selected ? "action.selected" : "transparent",
          "&:hover": { bgcolor: "action.hover" }
        }}
      >
        <Box
          component="span"
          aria-hidden
          sx={{
            fontSize: 10,
            color: "text.secondary",
            transform: selected ? "rotate(180deg)" : "rotate(0deg)",
            transition: "transform 0.2s ease",
            width: 14,
            display: "inline-block"
          }}
        >
          ▼
        </Box>
        <Box sx={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 0.25 }}>
          <Typography
            variant="body2"
            sx={{ fontWeight: 600, lineHeight: 1.35, wordBreak: "break-word" }}
          >
            {method.description}
          </Typography>
          <Typography variant="caption" color="text.secondary" sx={{ fontFamily: "monospace" }}>
            {methodSubtitle(method)}
          </Typography>
        </Box>
      </Box>

      {selected && (
        <Box sx={{ borderTop: 1, borderColor: "divider", p: 1.5, display: "flex", flexDirection: "column", gap: 1 }}>
          <Typography variant="caption" color="text.secondary">
            Sample request — JSON array of arguments. Edit values and Run. Placeholders like{" "}
            <code>{"{{customerId}}"}</code> are filled from recent events.
          </Typography>
          <TextField
            multiline
            fullWidth
            minRows={4}
            value={text}
            onChange={(e) => setText(e.target.value)}
            sx={{ "& textarea": { fontFamily: "monospace", fontSize: 12 } }}
          />
          <Stack direction="row" spacing={1}>
            <Button variant="contained" size="small" onClick={onRun} disabled={busy}>
              {busy ? "Running…" : "Run"}
            </Button>
            <Button
              size="small"
              variant="outlined"
              onClick={() => setText(applyContextToSample(method.sampleRequest, ctx))}
            >
              Reset sample
            </Button>
          </Stack>
          {result && (
            <>
              <Divider sx={{ my: 0.5 }} />
              <Stack direction="row" spacing={1} alignItems="center">
                <Chip
                  size="small"
                  label={result.kind === "ok" ? "Response" : "Error"}
                  color={result.kind === "ok" ? "success" : "error"}
                />
                <Typography variant="caption" color="text.secondary">
                  {formatTs(result.ts)}
                </Typography>
              </Stack>
              <Box
                component="pre"
                sx={{
                  m: 0,
                  p: 1,
                  bgcolor: "action.hover",
                  borderRadius: 1,
                  fontSize: 12,
                  overflow: "auto",
                  maxHeight: 280
                }}
              >
                {result.kind === "ok" ? safeJson(result.value) : result.message}
              </Box>
            </>
          )}
        </Box>
      )}
    </Paper>
  );
}

export function InvokeMethodPanel({
  ctx,
  clientRef
}: {
  ctx: AafEventContext;
  clientRef: MutableRefObject<AafClient | null>;
}) {
  const [filter, setFilter] = useState("");
  const [selectedKey, setSelectedKey] = useState<string | null>(null);
  const [manualOpen, setManualOpen] = useState(false);

  const filtered = useMemo(() => {
    const q = filter.trim().toLowerCase();
    if (!q) return AAF_METHODS;
    return AAF_METHODS.filter(
      (m) =>
        methodSubtitle(m).toLowerCase().includes(q) ||
        m.description.toLowerCase().includes(q)
    );
  }, [filter]);

  return (
    <Stack spacing={1.5}>
      <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap">
        <Button variant="contained" size="small" onClick={() => setManualOpen(true)}>
          Invoke any method manually
        </Button>
        <TextField
          size="small"
          placeholder="Search method (api or name)…"
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
          sx={{ minWidth: 240, flex: 1 }}
        />
      </Stack>

      <Alert severity="info" variant="outlined">
        Each method runs against the live <code>AmeyoClient</code>. Switch ids / phone numbers to
        ones your campaign actually has — server returns 121/122 if the values are not valid.
      </Alert>

      <Stack spacing={1}>
        {filtered.map((m, idx) => {
          const key = `${m.api}.${m.name}#${idx}`;
          return (
            <MethodCard
              key={key}
              method={m}
              ctx={ctx}
              selected={selectedKey === key}
              onToggle={() => setSelectedKey((prev) => (prev === key ? null : key))}
              clientRef={clientRef}
            />
          );
        })}
      </Stack>

      <ManualInvokeDialog
        open={manualOpen}
        onClose={() => setManualOpen(false)}
        clientRef={clientRef}
        ctx={ctx}
      />
    </Stack>
  );
}

function ManualInvokeDialog({
  open,
  onClose,
  clientRef,
  ctx
}: {
  open: boolean;
  onClose: () => void;
  clientRef: MutableRefObject<AafClient | null>;
  ctx: AafEventContext;
}) {
  const [api, setApi] = useState<AafApiNamespace>("globalData");
  const [methodName, setMethodName] = useState<string>("get");
  const [body, setBody] = useState<string>('[\n  "loggedInUser"\n]');
  const [busy, setBusy] = useState(false);
  const [result, setResult] = useState<MethodResult | null>(null);

  const onRun = () => {
    const client = clientRef.current;
    if (!client) {
      setResult({ kind: "error", ts: Date.now(), message: "AmeyoClient not ready yet." });
      return;
    }
    const parsed = tryParseJsonArgs(applyContextToSample(body, ctx));
    if (!Array.isArray(parsed)) {
      setResult({ kind: "error", ts: Date.now(), message: `Invalid JSON: ${parsed.error}` });
      return;
    }
    const name = methodName.trim();
    if (!name) {
      setResult({ kind: "error", ts: Date.now(), message: "Method name is required." });
      return;
    }
    setBusy(true);
    setResult(null);
    callApiMethod(client, api, name, parsed)
      .then((value) => setResult({ kind: "ok", ts: Date.now(), value }))
      .catch((err: unknown) =>
        setResult({
          kind: "error",
          ts: Date.now(),
          message: err instanceof Error ? err.message : safeJson(err)
        })
      )
      .finally(() => setBusy(false));
  };

  return (
    <StructuredDialog
      open={open}
      onClose={onClose}
      title="Invoke any method"
      showFooter
      footerContent={
        <>
          <Button variant="outlined" size="small" onClick={onClose}>
            Close
          </Button>
          <Button variant="contained" size="small" onClick={onRun} disabled={busy}>
            {busy ? "Running…" : "Run"}
          </Button>
        </>
      }
    >
      <Stack spacing={2}>
        <Box>
          <Typography variant="caption" color="text.secondary">
            API namespace
          </Typography>
          <Select
            size="small"
            fullWidth
            value={api}
            onChange={(e: { target: { value: unknown } }) =>
              setApi(String(e.target.value) as AafApiNamespace)
            }
          >
            {AAF_API_NAMESPACES.map((ns) => (
              <MenuItem key={ns} value={ns}>
                {ns}
              </MenuItem>
            ))}
          </Select>
        </Box>
        <Box>
          <Typography variant="caption" color="text.secondary">
            Method name (e.g. <code>get</code>, <code>trigger</code>, <code>register</code>)
          </Typography>
          <TextField
            size="small"
            fullWidth
            value={methodName}
            onChange={(e) => setMethodName(e.target.value)}
            placeholder="get"
          />
        </Box>
        <Box>
          <Typography variant="caption" color="text.secondary">
            Arguments — JSON array (placeholders like <code>{"{{customerId}}"}</code> are substituted)
          </Typography>
          <TextField
            multiline
            minRows={6}
            fullWidth
            value={body}
            onChange={(e) => setBody(e.target.value)}
            sx={{ "& textarea": { fontFamily: "monospace", fontSize: 12 } }}
          />
        </Box>
        {result && (
          <>
            <Stack direction="row" spacing={1} alignItems="center">
              <Chip
                size="small"
                label={result.kind === "ok" ? "Response" : "Error"}
                color={result.kind === "ok" ? "success" : "error"}
              />
              <Typography variant="caption" color="text.secondary">
                {formatTs(result.ts)}
              </Typography>
            </Stack>
            <Box
              component="pre"
              sx={{
                m: 0,
                p: 1,
                bgcolor: "action.hover",
                borderRadius: 1,
                fontSize: 12,
                overflow: "auto",
                maxHeight: 240
              }}
            >
              {result.kind === "ok" ? safeJson(result.value) : result.message}
            </Box>
          </>
        )}
      </Stack>
    </StructuredDialog>
  );
}

export { ManualInvokeDialog };
