"use client";

import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import type { Task, TaskStatus } from "@slackwsh/contracts";
import { AppShell, useWorkspaceIdParam } from "../../components/AppShell";
import { TaskModal, type TaskModalValues } from "../../components/TaskModal";
import { api, getCurrentUserId } from "../../lib/api";
import { avatarColor, initials } from "../../lib/avatar";
import { dmTitle, isDmChannel, isTextChannel, normalizeChannelRows, type ConversationRow } from "../../lib/conversations";
import { IconPlus } from "../../components/icons";

interface MemberRow {
  user: { id: string | number; name: string; email: string };
}

type TaskTab = TaskStatus | "deleted";

const TABS: Array<{ id: TaskTab; label: string }> = [
  { id: "todo", label: "To do" },
  { id: "in_progress", label: "In progress" },
  { id: "done", label: "Done" },
  { id: "deleted", label: "Deleted" },
];

function isOverdue(task: Task) {
  return Boolean(task.dueAt) && task.status !== "done" && !task.deletedAt && new Date(task.dueAt!).getTime() < Date.now();
}

function formatDueDate(iso: string) {
  return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" });
}

type ModalState = { mode: "create" } | { mode: "edit"; task: Task } | null;

function TasksView() {
  const workspaceId = useWorkspaceIdParam();
  const [tasks, setTasks] = useState<Task[]>([]);
  const [loading, setLoading] = useState(true);
  const [statusFilter, setStatusFilter] = useState<TaskTab>("todo");
  const [mineOnly, setMineOnly] = useState(false);
  const showDeleted = statusFilter === "deleted";
  const [members, setMembers] = useState<MemberRow[]>([]);
  const [conversations, setConversations] = useState<ConversationRow[]>([]);
  const [myUserId, setMyUserId] = useState<number | null>(null);
  const [modal, setModal] = useState<ModalState>(null);
  const [saveError, setSaveError] = useState<string | null>(null);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    getCurrentUserId().then(setMyUserId);
  }, []);

  useEffect(() => {
    if (!workspaceId) return;
    api
      .members(workspaceId)
      .then((res) => setMembers(res.members as MemberRow[]))
      .catch(() => undefined);
    api
      .listChannels(workspaceId)
      .then((res) => setConversations(normalizeChannelRows(res.channels)))
      .catch(() => undefined);
  }, [workspaceId]);

  const load = useCallback(() => {
    if (!workspaceId) return;
    setLoading(true);
    api
      .listTasks(workspaceId, {
        status: showDeleted ? undefined : statusFilter,
        deleted: showDeleted || undefined,
        mine: mineOnly || undefined,
      })
      .then((res) => setTasks(res.tasks))
      .catch(() => setTasks([]))
      .finally(() => setLoading(false));
  }, [workspaceId, statusFilter, showDeleted, mineOnly]);

  useEffect(() => {
    load();
  }, [load]);

  // Realtime: any task event for this workspace just triggers a refetch of
  // the current tab — simpler and just as correct as a field-level merge,
  // since the server-side filter (status/mine) decides what belongs in view
  // anyway. See RealtimeProvider's "slackwsh:task" re-dispatch.
  useEffect(() => {
    function onTaskEvent(e: Event) {
      // task:created/task:updated carry `{ task }`; only task:deleted carries a
      // bare workspaceId — so the id has to be read from either shape.
      const detail = (e as CustomEvent).detail as { workspaceId?: number; task?: Task } | undefined;
      const eventWorkspaceId = detail?.task?.workspaceId ?? detail?.workspaceId;
      if (!workspaceId || String(eventWorkspaceId) !== String(workspaceId)) return;
      load();
    }
    window.addEventListener("slackwsh:task", onTaskEvent);
    return () => window.removeEventListener("slackwsh:task", onTaskEvent);
  }, [workspaceId, load]);

  // Opening the page is the acknowledgement — the Tasks badge clears here
  // rather than per-row, matching how missed calls behave. Re-run on new
  // task events while the page is visible so a live assignment never sticks.
  useEffect(() => {
    if (!workspaceId) return;
    let cancelled = false;
    function markSeen() {
      if (document.hidden) return;
      api
        .markAssignedTasksSeen(workspaceId!)
        .then((res) => {
          if (!cancelled && res.seen > 0) window.dispatchEvent(new CustomEvent("slackwsh:tasks-seen"));
        })
        .catch(() => undefined);
    }
    markSeen();
    function onTaskEvent(e: Event) {
      const detail = (e as CustomEvent).detail as { workspaceId?: number; task?: Task } | undefined;
      const eventWorkspaceId = detail?.task?.workspaceId ?? detail?.workspaceId;
      if (String(eventWorkspaceId) !== String(workspaceId)) return;
      markSeen();
    }
    function onVis() {
      if (!document.hidden) markSeen();
    }
    window.addEventListener("slackwsh:task", onTaskEvent);
    document.addEventListener("visibilitychange", onVis);
    return () => {
      cancelled = true;
      window.removeEventListener("slackwsh:task", onTaskEvent);
      document.removeEventListener("visibilitychange", onVis);
    };
  }, [workspaceId]);

  const nameById = useMemo(() => new Map(members.map((row) => [String(row.user.id), row.user.name])), [members]);

  // Only rooms can be *picked* as a task's channel; a DM only gets attached by
  // "create task from this message" inside that DM.
  const channels = useMemo(() => conversations.filter((row) => isTextChannel(row.channel.type)), [conversations]);

  // Labels cover DMs too, so a task sourced from a DM still shows where it came
  // from rather than silently rendering no source at all.
  const channelLabelById = useMemo(
    () =>
      new Map(
        conversations.map((row) => [
          String(row.channel.id),
          isDmChannel(row.channel.type) ? dmTitle(row) : `#${row.channel.name ?? "untitled"}`,
        ]),
      ),
    [conversations],
  );

  function openCreate() {
    setSaveError(null);
    setModal({ mode: "create" });
  }

  function openEdit(task: Task) {
    if (task.deletedAt) return;
    setSaveError(null);
    setModal({ mode: "edit", task });
  }

  async function handleRestore(task: Task) {
    if (!workspaceId || !task.deletedAt) return;
    setSaving(true);
    try {
      await api.restoreTask(workspaceId, String(task.id));
      load();
    } catch {
      // Keep the list as-is; a failed restore leaves the row in Deleted.
    } finally {
      setSaving(false);
    }
  }

  async function handleSubmit(values: TaskModalValues) {
    if (!workspaceId) return;
    setSaving(true);
    setSaveError(null);
    try {
      if (modal?.mode === "create") {
        await api.createTask(workspaceId, {
          title: values.title,
          description: values.description || undefined,
          assigneeUserId: values.assigneeUserId,
          dueAt: values.dueAt,
          channelId: values.channelId,
        });
      } else if (modal?.mode === "edit") {
        const original = modal.task;
        // Only send the content patch when a content field actually changed:
        // `task:update` is creator/admin-only, while `task:update_status` is
        // open to the assignee, so an unconditional update call would 403 an
        // assignee who only wanted to move the task to another column.
        const patch: Parameters<typeof api.updateTask>[2] = {};
        if (values.title !== original.title) patch.title = values.title;
        if (values.description !== (original.description ?? "")) patch.description = values.description || null;
        if (values.dueAt !== original.dueAt) patch.dueAt = values.dueAt;
        if (values.channelId !== (original.channelId != null ? String(original.channelId) : null)) {
          patch.channelId = values.channelId;
        }
        if (Object.keys(patch).length > 0) await api.updateTask(workspaceId, String(original.id), patch);
        if (values.assigneeUserId !== (original.assigneeUserId != null ? String(original.assigneeUserId) : null)) {
          await api.assignTask(workspaceId, String(original.id), values.assigneeUserId);
        }
        if (values.status !== original.status) {
          await api.updateTaskStatus(workspaceId, String(original.id), values.status);
        }
      }
      setModal(null);
      load();
    } catch (err) {
      setSaveError(err instanceof Error ? err.message : "Couldn't save this task.");
    } finally {
      setSaving(false);
    }
  }

  async function handleDelete() {
    if (!workspaceId || modal?.mode !== "edit") return;
    if (!window.confirm(`Delete “${modal.task.title}”? You can find it later under Deleted.`)) return;
    setSaving(true);
    setSaveError(null);
    try {
      await api.deleteTask(workspaceId, String(modal.task.id));
      setModal(null);
      load();
    } catch (err) {
      setSaveError(err instanceof Error ? err.message : "Couldn't delete this task.");
    } finally {
      setSaving(false);
    }
  }

  return (
    <AppShell
      active="tasks"
      title="Tasks"
      subtitle={loading ? "Loading…" : `${tasks.length} task${tasks.length === 1 ? "" : "s"}`}
      actions={
        showDeleted ? undefined : (
          <button className="head-action primary" type="button" onClick={openCreate}>
            <IconPlus size={14} />
            <span>New task</span>
          </button>
        )
      }
    >
      <div className="task-tabs">
        {/* The status tabs are a real tablist; the "Assigned to me" filter is a
            toggle button and deliberately sits outside it — a tablist may only
            contain tabs. */}
        <div className="task-tablist" role="tablist" aria-label="Task status">
          {TABS.map((tab) => (
            <button
              key={tab.id}
              type="button"
              role="tab"
              id={`task-tab-${tab.id}`}
              aria-selected={statusFilter === tab.id}
              aria-controls="task-panel"
              tabIndex={statusFilter === tab.id ? 0 : -1}
              className={statusFilter === tab.id ? "task-tab active" : "task-tab"}
              onClick={() => setStatusFilter(tab.id)}
            >
              {tab.label}
            </button>
          ))}
        </div>
        <button
          type="button"
          className={mineOnly ? "task-tab mine active" : "task-tab mine"}
          aria-pressed={mineOnly}
          onClick={() => setMineOnly((v) => !v)}
        >
          Assigned to me
        </button>
      </div>

      <div className="task-body" id="task-panel" role="tabpanel" aria-labelledby={`task-tab-${statusFilter}`}>
        {loading && <div className="empty-state">Loading tasks…</div>}
        {!loading && tasks.length === 0 && (
          <div className="empty-state">
            {showDeleted
              ? mineOnly
                ? "No deleted tasks assigned to you."
                : "No deleted tasks."
              : statusFilter === "todo"
                ? mineOnly
                  ? "Nothing assigned to you right now."
                  : "No open tasks yet — create one to get started."
                : `No ${TABS.find((t) => t.id === statusFilter)?.label.toLowerCase()} tasks.`}
          </div>
        )}

        <div className="task-list">
          {tasks.map((task) => {
            const assigneeName = task.assigneeUserId != null ? nameById.get(String(task.assigneeUserId)) ?? "Someone" : null;
            const color = avatarColor(task.assigneeUserId != null ? String(task.assigneeUserId) : null);
            const overdue = isOverdue(task);
            const channelLabel = task.channelId != null ? channelLabelById.get(String(task.channelId)) : null;
            const deletedByName =
              task.deletedBy != null
                ? myUserId != null && String(task.deletedBy) === String(myUserId)
                  ? "you"
                  : nameById.get(String(task.deletedBy)) ?? "someone"
                : null;
            return (
              <div key={task.id} className="task-row-wrap">
                <button
                  type="button"
                  className={showDeleted ? "task-row task-row-deleted" : "task-row"}
                  onClick={() => openEdit(task)}
                  disabled={showDeleted}
                >
                  <span className="task-row-avatar" style={{ background: color.bg, color: color.fg }}>
                    {assigneeName ? initials(assigneeName) : "—"}
                  </span>
                  <span className="task-row-body">
                    <span className="task-row-title">{task.title}</span>
                    <span className="task-row-meta">
                      {showDeleted ? (
                        <>
                          <span>
                            Deleted
                            {task.deletedAt ? ` ${formatDueDate(task.deletedAt)}` : ""}
                            {deletedByName ? ` by ${deletedByName}` : ""}
                          </span>
                          {assigneeName && (
                            <>
                              <span className="dotsep">·</span>
                              <span>{assigneeName}</span>
                            </>
                          )}
                        </>
                      ) : (
                        <>
                          {assigneeName ? <span>{assigneeName}</span> : <span>Unassigned</span>}
                          <span className="dotsep">·</span>
                          <span>
                            Created by{" "}
                            {myUserId != null && String(task.createdBy) === String(myUserId)
                              ? "you"
                              : nameById.get(String(task.createdBy)) ?? "someone"}
                          </span>
                          {channelLabel && (
                            <>
                              <span className="dotsep">·</span>
                              <span>{channelLabel}</span>
                            </>
                          )}
                          {task.dueAt && (
                            <>
                              <span className="dotsep">·</span>
                              <span className={overdue ? "task-due overdue" : "task-due"}>{formatDueDate(task.dueAt)}</span>
                            </>
                          )}
                        </>
                      )}
                    </span>
                  </span>
                  {!showDeleted && (
                    <span className={`task-status-pill ${task.status}`}>
                      {task.status === "todo" ? "To do" : task.status === "in_progress" ? "In progress" : "Done"}
                    </span>
                  )}
                </button>
                {showDeleted && (
                  <button
                    type="button"
                    className="task-restore-btn"
                    disabled={saving}
                    onClick={() => void handleRestore(task)}
                  >
                    Restore
                  </button>
                )}
              </div>
            );
          })}
        </div>
      </div>

      {modal && (
        <TaskModal
          title={modal.mode === "create" ? "New task" : "Edit task"}
          mode={modal.mode}
          busy={saving}
          error={saveError}
          initial={
            modal.mode === "edit"
              ? {
                  title: modal.task.title,
                  description: modal.task.description ?? "",
                  assigneeUserId: modal.task.assigneeUserId != null ? String(modal.task.assigneeUserId) : null,
                  dueAt: modal.task.dueAt,
                  channelId: modal.task.channelId != null ? String(modal.task.channelId) : null,
                  status: modal.task.status,
                }
              : { assigneeUserId: myUserId != null ? String(myUserId) : null }
          }
          members={members}
          channels={channels}
          // A DM isn't in the room picker, so editing a DM-sourced task would
          // show an empty picker and silently drop the link on save. Show it
          // read-only instead.
          lockedChannelName={
            modal.mode === "edit" && modal.task.channelId != null && !channels.some((row) => String(row.channel.id) === String(modal.task.channelId))
              ? channelLabelById.get(String(modal.task.channelId)) ?? "Direct message"
              : undefined
          }
          createdByName={
            modal.mode === "edit"
              ? myUserId != null && String(modal.task.createdBy) === String(myUserId)
                ? "you"
                : nameById.get(String(modal.task.createdBy)) ?? "Someone"
              : undefined
          }
          onClose={() => setModal(null)}
          onSubmit={handleSubmit}
          onDelete={modal.mode === "edit" ? handleDelete : undefined}
        />
      )}
    </AppShell>
  );
}

export default function TasksPage() {
  return (
    <Suspense fallback={<main className="page"><div className="empty-state">Loading tasks…</div></main>}>
      <TasksView />
    </Suspense>
  );
}
