import { create } from 'zustand';
import { persist } from 'zustand/middleware';

import { TITLE_MAX_LENGTH, type GlideDecision, type GlideTaskStatus, type TaskId } from '@/domain/glide';

import { newTaskId } from './ids';
import { mmkvJSONStorage } from './storage';
import { currentOwnerId } from './sync/owner';

/**
 * The persisted shape of a task — a superset of the domain `GlideTask`. It carries the
 * timestamps the swipe loop needs plus the sync metadata the Convex sync engine reconciles
 * on: a client-owned `id`, the account `ownerId`, an `updatedAt` (the last-write-wins key),
 * a `syncedAt` (the `updatedAt` the server last confirmed — a row is *dirty* when
 * `updatedAt > syncedAt`), and a `deletedAt` tombstone. Review/action order derives from
 * `createdAt`/`completedAt`. See docs/engineering/convex-sync.md.
 */
export type LocalTask = {
  id: TaskId;
  /** Account scope. `null` until adopted (signed-out rows); a fixed constant in dogfood mode. */
  ownerId: string | null;
  title: string;
  resistance: number;
  status: GlideTaskStatus;
  exposureCount: number;
  createdAt: number;
  updatedAt: number;
  lastDecision?: GlideDecision;
  completedAt?: number;
  /** Soft-delete tombstone — kept so sync can propagate deletes. */
  deletedAt?: number;
  /** The `updatedAt` the server last confirmed; `null` = never synced. Dirty iff `updatedAt > syncedAt`. */
  syncedAt: number | null;
};

/**
 * A task as the server hands it back (synced fields, keyed by `clientId`). Convex's
 * `Doc<'tasks'>` satisfies this structurally — the sync engine passes its query results
 * straight in. Kept here so the store doesn't import `convex/_generated`.
 */
export type ServerTask = {
  clientId: TaskId;
  ownerId: string;
  title: string;
  status: GlideTaskStatus;
  resistance: number;
  exposureCount: number;
  createdAt: number;
  updatedAt: number;
  lastDecision?: GlideDecision;
  completedAt?: number;
  deletedAt?: number;
};

/** A row has unsynced local edits iff its updatedAt is ahead of what the server confirmed. */
export const isDirty = (t: LocalTask): boolean => t.updatedAt > (t.syncedAt ?? -1);

function serverToLocal(r: ServerTask): LocalTask {
  return {
    id: r.clientId,
    ownerId: r.ownerId,
    title: r.title,
    status: r.status,
    resistance: r.resistance,
    exposureCount: r.exposureCount,
    createdAt: r.createdAt,
    updatedAt: r.updatedAt,
    lastDecision: r.lastDecision,
    completedAt: r.completedAt,
    deletedAt: r.deletedAt,
    syncedAt: r.updatedAt, // server is authoritative → this row is clean
  };
}

/**
 * The current action session. Counts what's been actioned since the last Plan
 * session began. This is inherently *this device's* state — it is deliberately
 * NOT part of the synced task collection.
 */
export type SessionState = {
  doneThisSession: number;
  planSessionAt: number;
};

/** A handful of title-only tasks so a fresh install has something to plan. */
const STARTER_TASKS = [
  { title: 'Reply to the message you keep ignoring', resistance: 3 },
  { title: 'Clear the right side of the desk', resistance: 1 },
  { title: 'Open the tax folder', resistance: 5 },
  { title: 'Put tomorrow’s walk on the calendar', resistance: 2 },
  { title: 'Draft the note you’ve been avoiding', resistance: 4 },
] as const;

type TaskState = {
  tasks: LocalTask[];
  session: SessionState;
  /** Capture a new open task from a title. Trims + caps; empty titles are ignored. */
  createTask: (title: string) => void;
  /** Record a planning decision: `clear` readies the task, `resistant` softens + re-sinks it. */
  assess: (id: TaskId, decision: GlideDecision) => void;
  /** Mark a ready task done and count it toward this session's tally. */
  complete: (id: TaskId) => void;
  /** Begin a Plan session: zero the action tally. */
  startPlanSession: () => void;
  /** Seed the starter tasks once, only when the store is completely empty. */
  ensureStarterTasks: () => void;
  /** Sync: merge authoritative server rows in (last-write-wins; keeps newer local edits). */
  ingestServerRows: (rows: ServerTask[]) => void;
  /** Sync: claim un-owned local rows into an account (the sign-in adoption path). */
  adoptOwner: (ownerId: string) => void;
};

/**
 * The local source of truth. Every screen reads/writes here (via `useBoard` and the
 * `task-repository` seam), never Convex directly — so the whole app works offline, with no
 * account and no network. Persisted to MMKV; synchronous hydration means the data is
 * present on the first render. The Convex sync engine (src/data/sync) mirrors this store
 * to the server in the background when online + signed in.
 */
export const useTaskStore = create<TaskState>()(
  persist(
    (set, get) => ({
      tasks: [],
      session: { doneThisSession: 0, planSessionAt: 0 },

      createTask: (rawTitle) => {
        const title = rawTitle.trim().slice(0, TITLE_MAX_LENGTH);
        if (title.length === 0) return;
        const now = Date.now();
        const task: LocalTask = {
          id: newTaskId(),
          ownerId: currentOwnerId(),
          title,
          resistance: 1,
          status: 'open',
          exposureCount: 0,
          createdAt: now,
          updatedAt: now,
          syncedAt: null,
        };
        set((state) => ({ tasks: [...state.tasks, task] }));
      },

      assess: (id, decision) => {
        const now = Date.now();
        set((state) => ({
          tasks: state.tasks.map((task): LocalTask => {
            if (task.id !== id || task.status === 'done') return task;
            const exposureCount = task.exposureCount + 1;
            if (decision === 'clear') {
              // No resistance: the task is ready to act on.
              return { ...task, status: 'ready', exposureCount, lastDecision: 'clear', updatedAt: now };
            }
            // Still resistant: soften it (bump resistance) but leave its place in the deck.
            // `sortAt` is intentionally NOT changed — the planning deck is ordered by it, so
            // keeping it equal to creation time makes the review order a stable oldest→newest.
            // The task simply returns in the next pass (seenIds resets when Plan re-opens).
            return {
              ...task,
              status: 'open',
              resistance: Math.min(task.resistance + 1, 5),
              exposureCount,
              lastDecision: 'resistant',
              updatedAt: now,
            };
          }),
        }));
      },

      complete: (id) => {
        const now = Date.now();
        let actioned = false;
        const tasks = get().tasks.map((task): LocalTask => {
          if (task.id !== id || task.status === 'done') return task;
          actioned = true;
          return { ...task, status: 'done', completedAt: now, updatedAt: now };
        });
        if (!actioned) return;
        set((state) => ({
          tasks,
          session: { ...state.session, doneThisSession: state.session.doneThisSession + 1 },
        }));
      },

      startPlanSession: () => {
        set({ session: { doneThisSession: 0, planSessionAt: Date.now() } });
      },

      ensureStarterTasks: () => {
        if (get().tasks.length > 0) return;
        const now = Date.now();
        const owner = currentOwnerId();
        const seeded: LocalTask[] = STARTER_TASKS.map((seed, index) => ({
          id: newTaskId(),
          ownerId: owner,
          title: seed.title,
          resistance: seed.resistance,
          status: 'open',
          exposureCount: 0,
          createdAt: now + index,
          updatedAt: now,
          syncedAt: null,
        }));
        set({ tasks: seeded });
      },

      ingestServerRows: (rows) => {
        if (rows.length === 0) return;
        set((state) => {
          const byId = new Map(state.tasks.map((t) => [t.id, t]));
          for (const row of rows) {
            const incoming = serverToLocal(row);
            const local = byId.get(incoming.id);
            // Adopt the server row when it's newer-or-equal (this also confirms our own
            // just-pushed edit → marks it clean). Keep a strictly-newer local row: it's a
            // dirty edit that hasn't pushed yet and will win when it does.
            if (!local || incoming.updatedAt >= local.updatedAt) {
              byId.set(incoming.id, incoming);
            }
          }
          return { tasks: [...byId.values()] };
        });
      },

      adoptOwner: (ownerId) => {
        const tasks = get().tasks;
        if (tasks.every((t) => t.ownerId === ownerId)) return;
        const now = Date.now();
        set({
          tasks: tasks.map((t) => (t.ownerId === ownerId ? t : { ...t, ownerId, updatedAt: now })),
        });
      },
    }),
    {
      name: 'glide.tasks',
      storage: mmkvJSONStorage,
    }
  )
);
