'use client';

import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useQuery, useMutation, useQueryClient, type QueryKey } from '@tanstack/react-query';
import { formatDistanceToNow } from 'date-fns';
import { Lock, Pencil, Trash2, Send, Loader2 } from 'lucide-react';
import { useAutoAnimate } from '@formkit/auto-animate/react';

import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { apiFetch } from '@/lib/api/client';
import { stageLabel } from '@/lib/constants';

type NoteSource =
  | 'client'
  | 'interest'
  | 'yacht'
  | 'company'
  | 'residential_client'
  | 'residential_interest';

interface Note {
  id: string;
  content: string;
  authorId: string;
  authorName?: string;
  isLocked: boolean;
  createdAt: string;
  updatedAt: string;
  /** Aggregated-mode only: which child entity this note came from. */
  source?: NoteSource;
  sourceId?: string;
  sourceLabel?: string;
  /** Pipeline stage the linked interest was at when the note was authored.
   *  Only populated for interest notes - drives the small stage chip. */
  pipelineStageAtCreation?: string | null;
}

type NotesEntityType =
  | 'clients'
  | 'interests'
  | 'yachts'
  | 'companies'
  | 'residential_clients'
  | 'residential_interests';

/** Maps the entity-type the list is rendered for to the `source` value
 *  the aggregator uses when a note came from THAT entity itself
 *  (vs. a related entity). Used to decide whether a note is editable
 *  in-place or read-only with an "Open source" affordance. */
const SELF_SOURCE: Record<NotesEntityType, NoteSource | null> = {
  clients: 'client',
  yachts: 'yacht',
  companies: 'company',
  residential_clients: 'residential_client',
  // Aggregate-mode is only meaningful for the entities above. Interests
  // and residential_interests are leaf nodes - there's nothing to roll
  // up to them.
  interests: null,
  residential_interests: null,
};

const AGGREGATABLE: ReadonlySet<NotesEntityType> = new Set<NotesEntityType>([
  'clients',
  'yachts',
  'companies',
  'residential_clients',
]);

/** Maps the entityType discriminator to its REST path segment. The
 *  residential entities use slash-separated routes
 *  (`/api/v1/residential/clients/...`), so the raw underscore discriminator
 *  must NOT be interpolated into the URL — doing so 404'd every residential
 *  notes request and the UI silently showed "No notes yet" (audit H7). */
const NOTES_API_PATH: Record<NotesEntityType, string> = {
  clients: 'clients',
  interests: 'interests',
  yachts: 'yachts',
  companies: 'companies',
  residential_clients: 'residential/clients',
  residential_interests: 'residential/interests',
};

const SOURCE_BADGE_CLASS: Record<NoteSource, string> = {
  client: 'bg-violet-100 text-violet-900',
  interest: 'bg-blue-100 text-blue-900',
  yacht: 'bg-emerald-100 text-emerald-900',
  company: 'bg-amber-100 text-amber-900',
  residential_client: 'bg-violet-100 text-violet-900',
  residential_interest: 'bg-blue-100 text-blue-900',
};

const SOURCE_LABEL: Record<NoteSource, string> = {
  client: 'Client',
  interest: 'Interest',
  yacht: 'Yacht',
  company: 'Company',
  residential_client: 'Resident',
  residential_interest: 'Inquiry',
};

interface NotesListProps {
  entityType: NotesEntityType;
  entityId: string;
  currentUserId?: string;
  /**
   * Aggregate-on-read: union the entity's own notes with notes from
   * related entities (interests, owned yachts / company yachts, owner
   * client). Cross-source notes render with a source chip and are
   * read-only here - open the source entity's page to edit.
   *
   * Supported for entityType in {clients, yachts, companies,
   * residential_clients}. Ignored for interests / residential_interests.
   */
  aggregate?: boolean;
  /**
   * Optional parent-entity query key to invalidate alongside the notes
   * query on create/update/delete. The parent entity detail typically
   * hydrates a `recentNote` / `notesCount` teaser that goes stale after
   * a note mutation; passing the detail's query key here keeps it in
   * sync without a hard refresh.
   */
  parentInvalidateKey?: QueryKey;
}

const NOTE_EDIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes

/** Sort by source then chronologically inside each source.
 *  Used by the aggregated view's "Group by source" toggle. */
function sortByGroup(notes: Note[]): Note[] {
  const sourceOrder: Record<string, number> = {
    client: 0,
    company: 1,
    yacht: 2,
    interest: 3,
    residential_client: 0,
    residential_interest: 1,
  };
  return [...notes].sort((a, b) => {
    const aRank = sourceOrder[a.source ?? ''] ?? 99;
    const bRank = sourceOrder[b.source ?? ''] ?? 99;
    if (aRank !== bRank) return aRank - bRank;
    const aLabel = a.sourceLabel ?? '';
    const bLabel = b.sourceLabel ?? '';
    if (aLabel !== bLabel) return aLabel.localeCompare(bLabel);
    return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
  });
}

/** Resolve the detail-page URL for a note's source entity so the
 *  aggregated-mode source badge can navigate the rep to that record. */
function sourceLinkFor(portSlug: string, source: NoteSource, sourceId: string): string | null {
  if (!portSlug) return null;
  switch (source) {
    case 'client':
      return `/${portSlug}/clients/${sourceId}`;
    case 'company':
      return `/${portSlug}/companies/${sourceId}`;
    case 'yacht':
      return `/${portSlug}/yachts/${sourceId}`;
    case 'interest':
      return `/${portSlug}/interests/${sourceId}`;
    case 'residential_client':
      return `/${portSlug}/residential/clients/${sourceId}`;
    case 'residential_interest':
      return `/${portSlug}/residential/interests/${sourceId}`;
    default:
      return null;
  }
}

export function NotesList({
  entityType,
  entityId,
  currentUserId,
  aggregate,
  parentInvalidateKey,
}: NotesListProps) {
  const queryClient = useQueryClient();
  const routeParams = useParams<{ portSlug: string }>();
  const portSlug = routeParams?.portSlug ?? '';
  const invalidateAll = () => {
    queryClient.invalidateQueries({ queryKey });
    if (parentInvalidateKey) {
      queryClient.invalidateQueries({ queryKey: parentInvalidateKey });
    }
  };
  const [newNote, setNewNote] = useState('');
  const [editingId, setEditingId] = useState<string | null>(null);
  const [editContent, setEditContent] = useState('');
  const [groupBySource, setGroupBySource] = useState(false);
  // Wall-clock 'now' ticked every 30s so the per-note "Xm left to edit"
  // countdown decrements on screen. Reading `Date.now()` directly inside
  // render is impure (different value every call); pinning to a state
  // value means React Compiler can memoize cleanly.
  // The interval is scheduled below ONLY while at least one note is still
  // inside its 15-min edit window — see `anyNoteWithinEditWindow`. An idle
  // NotesList (every note past its window, or none editable by this user)
  // burns no timer and triggers no re-renders.
  const [now, setNow] = useState(() => Date.now());

  const aggregateOn = !!aggregate && AGGREGATABLE.has(entityType);
  const baseEndpoint = `/api/v1/${NOTES_API_PATH[entityType]}/${entityId}/notes`;
  const listEndpoint = aggregateOn ? `${baseEndpoint}?aggregate=true` : baseEndpoint;
  const queryKey = [entityType, entityId, 'notes', aggregateOn ? 'aggregated' : 'own'];

  // Smooth animation when notes are added / edited / deleted - replaces
  // the abrupt re-render with a per-row fade/slide.
  const [animateRef] = useAutoAnimate<HTMLDivElement>();

  const { data: notes = [], isLoading } = useQuery<Note[]>({
    queryKey,
    queryFn: () => apiFetch<{ data: Note[] }>(listEndpoint).then((r) => r.data),
  });

  // Mutations always target the parent entity (client). Aggregated
  // notes from interests/yachts are read-only here - the rep edits
  // them on the source entity's page (we surface a "Open source" link
  // below). Keeping mutations against `baseEndpoint` keeps the POST
  // route handler clean.
  const createMutation = useMutation({
    mutationFn: (content: string) => apiFetch(baseEndpoint, { method: 'POST', body: { content } }),
    onSuccess: () => {
      invalidateAll();
      setNewNote('');
    },
  });

  const updateMutation = useMutation({
    mutationFn: ({ noteId, content }: { noteId: string; content: string }) =>
      apiFetch(`${baseEndpoint}/${noteId}`, { method: 'PATCH', body: { content } }),
    onSuccess: () => {
      invalidateAll();
      setEditingId(null);
    },
  });

  const deleteMutation = useMutation({
    mutationFn: (noteId: string) => apiFetch(`${baseEndpoint}/${noteId}`, { method: 'DELETE' }),
    onSuccess: () => invalidateAll(),
  });

  // Aggregated view: only notes from THIS entity itself are editable
  // in-place. Notes pulled in from related entities (e.g. interests
  // surfaced under a client) must be edited on the source page so the
  // owning entity's timeline records the change.
  const selfSource = SELF_SOURCE[entityType];

  function canEdit(note: Note): boolean {
    if (note.authorId !== currentUserId) return false;
    if (note.isLocked) return false;
    if (aggregateOn && note.source && note.source !== selfSource) return false;
    const elapsed = now - new Date(note.createdAt).getTime();
    return elapsed < NOTE_EDIT_WINDOW_MS;
  }

  function getTimeRemaining(note: Note): string | null {
    const elapsed = now - new Date(note.createdAt).getTime();
    const remaining = NOTE_EDIT_WINDOW_MS - elapsed;
    if (remaining <= 0) return null;
    const mins = Math.ceil(remaining / 60000);
    return `${mins}m left to edit`;
  }

  // Whether THIS user has any note still inside its 15-min edit window.
  // Mirrors `canEdit`'s non-time gates (author, not locked, self-source)
  // and adds the time check against the current `now`. Drives the countdown
  // interval below: it only runs while this is true, so a NotesList with
  // nothing editable doesn't re-render every 30s. Recomputed each tick, so
  // when the last editable note crosses the threshold this flips false and
  // the effect tears the interval down.
  const anyNoteWithinEditWindow = notes.some((note) => {
    if (note.authorId !== currentUserId) return false;
    if (note.isLocked) return false;
    if (aggregateOn && note.source && note.source !== selfSource) return false;
    const elapsed = now - new Date(note.createdAt).getTime();
    return elapsed < NOTE_EDIT_WINDOW_MS;
  });

  useEffect(() => {
    if (!anyNoteWithinEditWindow) return;
    const id = setInterval(() => setNow(Date.now()), 30_000);
    return () => clearInterval(id);
  }, [anyNoteWithinEditWindow]);

  return (
    <div className="space-y-4">
      {/* Create note form */}
      <div className="space-y-2">
        <Textarea
          placeholder="Add a note..."
          value={newNote}
          onChange={(e) => setNewNote(e.target.value)}
          rows={3}
        />
        <div className="flex justify-end">
          <Button
            size="sm"
            disabled={!newNote.trim() || createMutation.isPending}
            onClick={() => createMutation.mutate(newNote.trim())}
          >
            {createMutation.isPending ? (
              <Loader2 className="mr-1.5 h-4 w-4 animate-spin" aria-hidden />
            ) : (
              <Send className="mr-1.5 h-4 w-4" aria-hidden />
            )}
            Add Note
          </Button>
        </div>
      </div>

      {/* Aggregated-mode controls - sort toggle. Only renders when
       *  aggregation is on and there's actually content to group. */}
      {aggregateOn && notes.length > 0 && (
        <div className="flex items-center justify-end gap-2 text-xs text-muted-foreground">
          <span>View:</span>
          <button
            type="button"
            className={!groupBySource ? 'font-medium text-foreground' : 'hover:text-foreground'}
            onClick={() => setGroupBySource(false)}
          >
            Chronological
          </button>
          <span>·</span>
          <button
            type="button"
            className={groupBySource ? 'font-medium text-foreground' : 'hover:text-foreground'}
            onClick={() => setGroupBySource(true)}
          >
            Group by source
          </button>
        </div>
      )}

      {/* Notes list */}
      {isLoading ? (
        <div className="text-center py-8 text-muted-foreground">Loading notes...</div>
      ) : notes.length === 0 ? (
        <div className="text-center py-8 text-muted-foreground">No notes yet</div>
      ) : (
        <div ref={animateRef} className="space-y-3">
          {(groupBySource ? sortByGroup(notes) : notes).map((note) => (
            <div key={note.id} className="flex gap-3 p-3 rounded-lg border bg-card">
              <Avatar className="h-8 w-8 shrink-0">
                <AvatarFallback className="text-xs">
                  {(note.authorName ?? 'U').charAt(0).toUpperCase()}
                </AvatarFallback>
              </Avatar>
              <div className="flex-1 min-w-0 space-y-1">
                <div className="flex items-center gap-2 text-sm flex-wrap">
                  <span className="font-medium">{note.authorName ?? 'User'}</span>
                  <span className="text-muted-foreground">
                    {formatDistanceToNow(new Date(note.createdAt), { addSuffix: true })}
                  </span>
                  {aggregateOn &&
                    note.source &&
                    note.source !== SELF_SOURCE[entityType] &&
                    note.sourceLabel &&
                    (() => {
                      // Source badge links to the originating entity so reps
                      // can pivot from "this note about a linked yacht" to
                      // the yacht detail page directly. Falls back to a
                      // plain span when no sourceId is present (rare; aggregator
                      // returns it for every materialised note).
                      const sourceHref = note.sourceId
                        ? sourceLinkFor(portSlug, note.source, note.sourceId)
                        : null;
                      const className = `inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium ${SOURCE_BADGE_CLASS[note.source]} ${sourceHref ? 'hover:opacity-80 transition-opacity' : ''}`;
                      const body = `${SOURCE_LABEL[note.source]} · ${note.sourceLabel}`;
                      const title = `Open this ${note.source}`;
                      return sourceHref ? (
                        <Link
                          href={sourceHref as never}
                          className={className}
                          title={title}
                          onClick={(e) => e.stopPropagation()}
                        >
                          {body}
                        </Link>
                      ) : (
                        <span className={className} title={`From ${note.source}`}>
                          {body}
                        </span>
                      );
                    })()}
                  {/* Pipeline-stage stamp: shows what stage the linked
                      interest was at when the note was authored. Lets a
                      rep trace how the deal's notes evolved (concerns
                      raised at qualified vs after reservation). Only
                      populated for interest notes from 2026-05-15+. */}
                  {note.pipelineStageAtCreation && (
                    <span
                      className="inline-flex items-center rounded-full bg-indigo-50 px-1.5 py-0.5 text-xs font-medium text-indigo-900"
                      title="Pipeline stage when note was authored"
                    >
                      @ {stageLabel(note.pipelineStageAtCreation)}
                    </span>
                  )}
                  {note.isLocked && <Lock className="h-3 w-3 text-muted-foreground" aria-hidden />}
                  {canEdit(note) && (
                    <span className="text-xs text-muted-foreground">{getTimeRemaining(note)}</span>
                  )}
                </div>
                {editingId === note.id ? (
                  <div className="space-y-2">
                    <Textarea
                      value={editContent}
                      onChange={(e) => setEditContent(e.target.value)}
                      rows={3}
                    />
                    <div className="flex gap-2">
                      <Button
                        size="sm"
                        disabled={!editContent.trim() || updateMutation.isPending}
                        onClick={() =>
                          updateMutation.mutate({ noteId: note.id, content: editContent.trim() })
                        }
                      >
                        Save
                      </Button>
                      <Button size="sm" variant="ghost" onClick={() => setEditingId(null)}>
                        Cancel
                      </Button>
                    </div>
                  </div>
                ) : (
                  <p className="text-sm whitespace-pre-wrap">{note.content}</p>
                )}
              </div>
              {canEdit(note) && editingId !== note.id && (
                <div className="flex gap-1 shrink-0">
                  <Button
                    variant="ghost"
                    size="icon"
                    className="h-7 w-7"
                    onClick={() => {
                      setEditingId(note.id);
                      setEditContent(note.content);
                    }}
                  >
                    <Pencil className="h-3.5 w-3.5" aria-hidden />
                  </Button>
                  <Button
                    variant="ghost"
                    size="icon"
                    className="h-7 w-7 text-destructive"
                    onClick={() => deleteMutation.mutate(note.id)}
                  >
                    <Trash2 className="h-3.5 w-3.5" aria-hidden />
                  </Button>
                </div>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
