import React, { useMemo, useState } from "react";

import {
  Alert,
  Box,
  Card,
  CardContent,
  Chip,
  IconButton,
  ListSubheader,
  Menu,
  MenuItem,
  Paper,
  Typography,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";

import { api, getApiErrorMessage } from "../api";
import ViewStateNotice from "./ViewStateNotice";
import { JobApplication } from "../types";
import { useI18n } from "../i18n/I18nProvider";
import { useViewResource } from "../hooks/useViewResource";
import {
  GROUP_ENTRY_STATUS,
  PIPELINE_GROUPS,
  PIPELINE_STATUSES,
  PipelineGroup,
  PipelineStatus,
  groupOf,
  normalizeStatus,
  statusLabel,
  statusTone,
} from "../pipeline";

type Status = PipelineStatus;

// The board shows one column per group rather than one per stage: ten columns is unreadable, and
// the stage detail is preserved on each card's chip. "Other" collects custom statuses, which
// belong to no group, and only appears when something is actually in it.
type ColumnKey = PipelineGroup | "Other";

const TONE_PALETTE: Record<string, (theme: any) => string> = {
  error: (theme) => (theme.vars?.palette ?? theme.palette).error.main,
  warning: (theme) => (theme.vars?.palette ?? theme.palette).warning.main,
  success: (theme) => (theme.vars?.palette ?? theme.palette).success.main,
  info: (theme) => (theme.vars?.palette ?? theme.palette).info.main,
  primary: (theme) => (theme.vars?.palette ?? theme.palette).primary.main,
  default: (theme) => (theme.vars?.palette ?? theme.palette).primary.main,
};

function toneColor(theme: any, status: Status | "Other"): string {
  return TONE_PALETTE[statusTone(status)](theme);
}

// Mockup's Applied-column cards carry small skill-tag pills ("React", "TS") sourced from the
// job's own `tags` field (JSON array string) -- no new data, just parsed for display.
function parseTags(raw?: string): string[] {
  if (!raw) return [];
  try {
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string").slice(0, 2) : [];
  } catch {
    return [];
  }
}

type PillTone = "error" | "warning" | "success" | "info";

// Mockup's single status pill per card ("Reply due in 2d", "Tomorrow 14:00", "720k NOK / yr").
// Built entirely from fields the board already fetches (needsFollowUp/followUpAt/salary/deadline)
// -- there's no interview-datetime field on the board's JobApplication shape, so an "Interview
// tomorrow 14:00"-style pill isn't reproducible; deadline is used as the closest real fallback.
function cardPill(job: JobApplication, t: (key: any, params?: any) => string): { text: string; tone: PillTone } | null {
  if (job.needsFollowUp) {
    const due = job.followUpAt ? new Date(job.followUpAt) : null;
    if (!due || due.getTime() <= Date.now()) return { text: t("kanbanFollowUpNow"), tone: "error" };
    const days = Math.max(1, Math.ceil((due.getTime() - Date.now()) / 86400000));
    return { text: t("kanbanReplyDueIn", { days }), tone: "warning" };
  }
  if (job.salary) return { text: job.salary, tone: "success" };
  if (job.deadline) return { text: new Date(job.deadline).toLocaleDateString(), tone: "info" };
  return null;
}

export default function KanbanBoard() {
  const theme = useTheme();
  const palette = theme.vars?.palette ?? theme.palette;
  const customShadows = (theme.vars as any)?.customShadows ?? (theme as any).customShadows;
  const { t } = useI18n();
  const [dragJobId, setDragJobId] = useState<number | null>(null);
  const [dragOverColumn, setDragOverColumn] = useState<ColumnKey | null>(null);
  const [moveError, setMoveError] = useState("");
  const [announcement, setAnnouncement] = useState("");
  const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
  const [menuJobId, setMenuJobId] = useState<number | null>(null);

  const jobsResource = useViewResource(
    async () => {
      const response = await api.get<JobApplication[]>("/jobapplications/board");
      return response.data;
    },
    {
      initialData: [],
      errorMessage: t("kanbanLoadFailed"),
      deps: [t],
    },
  );

  const jobs = jobsResource.data;

  const groups = useMemo(() => {
    const map = new Map<ColumnKey, JobApplication[]>();
    PIPELINE_GROUPS.forEach((g) => map.set(g.key, []));
    map.set("Other", []);
    for (const j of jobs) {
      map.get(groupOf(j.status) ?? "Other")!.push(j);
    }
    map.forEach((arr, k) => {
      // Sort by stage first so a column reads in pipeline order (Saved before Preparing), then by
      // recency. Fall back to savedAt: unapplied jobs have no dateApplied, and treating that as
      // the epoch would sink them to the bottom.
      arr.sort((a, b) => {
        const stageDelta = PIPELINE_STATUSES.indexOf(normalizeStatus(a.status) as Status)
          - PIPELINE_STATUSES.indexOf(normalizeStatus(b.status) as Status);
        if (stageDelta !== 0) return stageDelta;
        return +new Date(b.dateApplied ?? b.savedAt) - +new Date(a.dateApplied ?? a.savedAt);
      });
      map.set(k, arr);
    });
    return map;
  }, [jobs]);

  // A group is not a status, so dropping onto a column applies that group's entry stage. Precise
  // stages stay on the card menu.
  const onDropTo = async (group: PipelineGroup) => {
    if (!dragJobId) return;
    const movingJobId = dragJobId;
    const status = GROUP_ENTRY_STATUS[group];
    setDragJobId(null);
    setDragOverColumn(null);
    setMoveError("");
    try {
      await api.patch(`/jobapplications/${movingJobId}/status`, { status });
      jobsResource.setData((prev) => prev.map((j) => (j.id === movingJobId ? { ...j, status } : j)));
      setAnnouncement(t("kanbanMoveSucceeded", { status: statusLabel(t, status) }));
    } catch (error: any) {
      setMoveError(getApiErrorMessage(error, t("kanbanMoveFailed")));
      setAnnouncement(t("kanbanMoveFailedAnnouncement"));
    }
  };

  const setStatus = async (id: number, status: Status) => {
    setMoveError("");
    try {
      await api.patch(`/jobapplications/${id}/status`, { status });
      jobsResource.setData((prev) => prev.map((j) => (j.id === id ? { ...j, status } : j)));
      setAnnouncement(t("kanbanMoveSucceeded", { status: statusLabel(t, status) }));
    } catch (error: any) {
      setMoveError(getApiErrorMessage(error, t("kanbanMoveFailed")));
      setAnnouncement(t("kanbanMoveFailedAnnouncement"));
    }
  };

  const currentMenuStatus = menuJobId == null ? null : normalizeStatus(jobs.find((j) => j.id === menuJobId)?.status ?? "");

  // "Other" (custom statuses) only earns a column when something is in it, and is not a drop
  // target — there is no single custom status to assign.
  const columns = useMemo(() => {
    const base: { key: ColumnKey; label: string; droppable: boolean }[] = PIPELINE_GROUPS.map((g) => ({
      key: g.key,
      label: t(g.labelKey as any),
      droppable: true,
    }));
    if ((groups.get("Other") ?? []).length > 0) {
      base.push({ key: "Other", label: t("kanbanGroupOther"), droppable: false });
    }
    return base;
  }, [groups, t]);

  return (
    <Box sx={{ mt: 2, width: "100%", minWidth: 0, maxWidth: "100%", overflowX: "clip", contain: "inline-size" }}>
      <Typography variant="body2" sx={{ color: "text.secondary", mb: 1 }}>
        {t("kanbanHint")}
      </Typography>

      <ViewStateNotice
        loading={jobsResource.loading}
        error={jobsResource.error}
        title={t("kanbanLoadFailed")}
        description={t("kanbanLoadFailedBody")}
        onRetry={jobsResource.reload}
      />

      {!jobsResource.loading && !jobsResource.error ? (
        <Box
          sx={{
            display: { xs: "flex", md: "grid" },
            width: "100%",
            maxWidth: "100%",
            minWidth: 0,
            gridTemplateColumns: { md: "repeat(3, 1fr)" },
            gap: 2,
            alignItems: "start",
            overflowX: { xs: "auto", md: "visible" },
            scrollSnapType: { xs: "x mandatory", md: "none" },
            pb: { xs: 1, md: 0 },
            WebkitOverflowScrolling: "touch",
          }}
        >
          {columns.map(({ key, label, droppable }) => {
            const list = groups.get(key) ?? [];
            // Colour the column by its entry stage so the header dot still carries meaning.
            const c = droppable ? toneColor(theme, GROUP_ENTRY_STATUS[key as PipelineGroup]) : palette.text.disabled;
            const isDragActive = dragJobId !== null;
            const isActiveTarget = dragOverColumn === key;
            return (
              <Paper
                key={key}
                component="section"
                role="group"
                aria-label={`${label} column${droppable ? "" : ", not a drop target"}`}
                aria-disabled={isDragActive && !droppable ? true : undefined}
                tabIndex={isDragActive ? 0 : -1}
                data-drop-state={isActiveTarget ? (droppable ? "active" : "invalid") : isDragActive ? (droppable ? "valid" : "invalid") : "idle"}
                onFocus={() => { if (isDragActive) setDragOverColumn(key); }}
                onBlur={() => { if (dragOverColumn === key) setDragOverColumn(null); }}
                onKeyDown={(event) => {
                  if (event.key === "Escape" && isDragActive) {
                    event.preventDefault(); setDragJobId(null); setDragOverColumn(null); setAnnouncement(t("kanbanKeyboardMoveCancelled"));
                  } else if ((event.key === "Enter" || event.key === " ") && isDragActive) {
                    event.preventDefault();
                    if (droppable) void onDropTo(key as PipelineGroup);
                    else setAnnouncement(t("kanbanInvalidDropTarget", { status: label }));
                  }
                }}
                onDragEnter={() => { if (isDragActive) setDragOverColumn(key); }}
                onDragLeave={(event) => { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setDragOverColumn(null); }}
                onDragOver={(e) => { if (droppable) e.preventDefault(); }}
                onDrop={() => { if (droppable) void onDropTo(key as PipelineGroup); }}
                sx={{
                  p: 1.5,
                  borderRadius: 3,
                  minHeight: 220,
                  flex: { xs: "0 0 85vw", md: "none" },
                  scrollSnapAlign: { xs: "start", md: "none" },
                  border: `1px ${isDragActive ? "dashed" : "solid"} ${isActiveTarget ? palette.primary.main : palette.divider}`,
                  outline: isActiveTarget ? `2px solid ${droppable ? palette.primary.main : palette.error.main}` : "none",
                  outlineOffset: 2,
                  opacity: isDragActive && !droppable ? 0.65 : 1,
                  backgroundColor: isActiveTarget && droppable ? palette.action.hover : palette.grey[100],
                  transition: "background-color .15s, border-color .15s, outline-color .15s, opacity .15s",
                  "&:focus-visible": { boxShadow: customShadows?.focus },
                }}
              >
                <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.25, px: 0.25 }}>
                  <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
                    <Box sx={{ width: 9, height: 9, borderRadius: "50%", backgroundColor: c, flexShrink: 0 }} />
                    <Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
                      {label}
                    </Typography>
                  </Box>
                  <Box
                    sx={{
                      minWidth: 22,
                      textAlign: "center",
                      px: 0.75,
                      py: 0.15,
                      borderRadius: 999,
                      backgroundColor: palette.grey[300],
                    }}
                  >
                    <Typography variant="caption" sx={{ color: "text.secondary", fontWeight: 700 }}>
                      {list.length}
                    </Typography>
                  </Box>
                </Box>

                <Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
                  {list.map((j) => {
                    const pill = cardPill(j, t);
                    const tags = parseTags(j.tags);
                    // The column is a group, so each card names its own stage — grouping collapses
                    // the columns, not the information.
                    const cardStatus = normalizeStatus(j.status);
                    const cardColor = toneColor(theme, cardStatus);
                    return (
                      <Card
                        key={j.id}
                        draggable
                        role="button"
                        tabIndex={0}
                        aria-pressed={dragJobId === j.id}
                        aria-label={t(dragJobId === j.id ? "kanbanCardPickedUpLabel" : "kanbanCardPickUpLabel", { title: j.jobTitle, status: statusLabel(t, j.status) })}
                        onKeyDown={(event) => {
                          if (event.key === "Escape" && dragJobId === j.id) {
                            event.preventDefault(); setDragJobId(null); setDragOverColumn(null); setAnnouncement(t("kanbanKeyboardMoveCancelled"));
                          } else if (event.key === " ") {
                            event.preventDefault();
                            const pickingUp = dragJobId !== j.id;
                            setDragJobId(pickingUp ? j.id : null);
                            setDragOverColumn(null);
                            setAnnouncement(pickingUp ? t("kanbanPickedUpAnnouncement", { title: j.jobTitle }) : t("kanbanKeyboardMoveCancelled"));
                          }
                        }}
                        onDragStart={() => { setDragJobId(j.id); setAnnouncement(t("kanbanDragStartedAnnouncement", { title: j.jobTitle })); }}
                        onDragEnd={() => { setDragJobId(null); setDragOverColumn(null); }}
                        sx={{
                          cursor: "grab",
                          borderRadius: 2.5,
                          borderLeft: `4px solid ${cardColor}`,
                          transition: "box-shadow .15s, transform .15s",
                          outline: dragJobId === j.id ? `2px solid ${palette.primary.main}` : "none",
                          outlineOffset: 2,
                          "&:hover": { boxShadow: 4, transform: "translateY(-1px)" },
                          "&:focus-visible": { boxShadow: customShadows?.focus },
                          "&:active": { cursor: "grabbing" },
                        }}
                      >
                        <CardContent sx={{ p: 1.25, "&:last-child": { pb: 1.25 } }}>
                          <Box sx={{ display: "flex", justifyContent: "space-between", gap: 1 }}>
                            <Typography sx={{ fontWeight: 800, lineHeight: 1.25 }}>
                              {j.jobTitle}
                            </Typography>
                            <IconButton
                              size="small"
                              aria-label={`Change status for ${j.jobTitle}`}
                              onClick={(e) => {
                                e.stopPropagation();
                                setMenuJobId(j.id);
                                setMenuAnchor(e.currentTarget);
                              }}
                            >
                              <MoreHorizIcon fontSize="small" />
                            </IconButton>
                          </Box>
                          <Typography variant="body2" sx={{ color: "text.secondary" }}>
                            {[j.company?.name, j.location].filter(Boolean).join(" · ")}
                          </Typography>

                          <Box sx={{ mt: 0.75 }}>
                            <Chip
                              size="small"
                              label={statusLabel(t, j.status)}
                              sx={{
                                height: 22,
                                fontWeight: 700,
                                backgroundColor: `color-mix(in srgb, ${cardColor} 14%, transparent)`,
                                color: cardColor,
                              }}
                            />
                          </Box>

                          {tags.length > 0 && (
                            <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5, mt: 0.75 }}>
                              {tags.map((tag) => (
                                <Chip
                                  key={tag}
                                  size="small"
                                  label={tag}
                                  sx={{ height: 22, backgroundColor: palette.action.hover, color: "primary.main", fontWeight: 700 }}
                                />
                              ))}
                            </Box>
                          )}

                          {pill && (
                            <Box sx={{ mt: 0.75 }}>
                              <Chip
                                size="small"
                                label={pill.text}
                                sx={{
                                  height: 24,
                                  fontWeight: 700,
                                  backgroundColor: `color-mix(in srgb, ${palette[pill.tone].main} 14%, transparent)`,
                                  color: palette[pill.tone].main,
                                }}
                              />
                            </Box>
                          )}

                          {j.daysSince != null ? (
                            <Typography variant="caption" sx={{ display: "block", color: "text.secondary", mt: 0.75 }}>
                              {t("kanbanAppliedAgo", { days: j.daysSince })}
                            </Typography>
                          ) : null}
                        </CardContent>
                      </Card>
                    );
                  })}
                  {list.length === 0 && (
                    <Typography variant="body2" sx={{ color: "text.secondary", py: 1 }}>
                      {t("kanbanDropHere")}
                    </Typography>
                  )}
                </Box>
              </Paper>
            );
          })}
        </Box>
      ) : null}

      {moveError ? <Alert severity="error" variant="outlined" sx={{ mt: 2, color: "text.primary" }} onClose={() => setMoveError("")}>{moveError}</Alert> : null}
      <Box component="span" sx={{ position: "absolute", width: "1px", height: "1px", p: 0, m: "-1px", overflow: "hidden", clip: "rect(0, 0, 0, 0)", whiteSpace: "nowrap", border: 0 }} aria-live="polite">
        {announcement}
      </Box>

      {/* Dragging is coarse (group entry stage only), so the menu carries every precise stage. */}
      <Menu anchorEl={menuAnchor} open={Boolean(menuAnchor)} onClose={() => { setMenuAnchor(null); setMenuJobId(null); }}>
        {PIPELINE_GROUPS.flatMap((g) => {
          const options = g.statuses.filter((s) => s !== currentMenuStatus);
          if (options.length === 0) return [];
          return [
            <ListSubheader key={`${g.key}-header`} sx={{ lineHeight: "32px", backgroundColor: "transparent" }}>
              {t(g.labelKey as any)}
            </ListSubheader>,
            ...options.map((s) => (
              <MenuItem key={s} onClick={() => { if (menuJobId) void setStatus(menuJobId, s); setMenuAnchor(null); setMenuJobId(null); }}>
                {t("jobTableSetStatus", { status: statusLabel(t, s) })}
              </MenuItem>
            )),
          ];
        })}
      </Menu>
    </Box>
  );
}
