// Local-first store — the single source of truth for the UI.
//
// Screens render synchronously from the in-memory state (hydrated from
// AsyncStorage at launch). Mutations apply locally immediately and enqueue an
// op; a background worker drains the queue to the API and then pulls the
// server state, so the network is never on the interaction path. Single-user
// last-write-wins: rows with queued local ops keep their local version during
// a pull; everything else takes the server's.
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Crypto from "expo-crypto";
import { api } from "./api";
import { getConfig } from "./config";
import type { Project, Task } from "./types";

export interface State {
  tasks: Task[];
  projects: Project[];
  hydrated: boolean;
  syncing: boolean;
  pending: number;
  syncError: string | null;
}

type Op =
  | { kind: "task.create" | "task.update"; id: string; data: Partial<Task> }
  | { kind: "project.create" | "project.update"; id: string; data: Partial<Project> };

const DATA_KEY = "store_v1";
const QUEUE_KEY = "queue_v1";

let state: State = {
  tasks: [],
  projects: [],
  hydrated: false,
  syncing: false,
  pending: 0,
  syncError: null,
};
let queue: Op[] = [];
const listeners = new Set<() => void>();

export function getState(): State {
  return state;
}

export function subscribe(listener: () => void): () => void {
  listeners.add(listener);
  return () => listeners.delete(listener);
}

function setState(patch: Partial<State>) {
  state = { ...state, ...patch, pending: queue.length };
  for (const l of listeners) l();
}

function persistData() {
  void AsyncStorage.setItem(DATA_KEY, JSON.stringify({ tasks: state.tasks, projects: state.projects }));
}

function persistQueue() {
  void AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
}

export async function hydrate() {
  if (state.hydrated) return;
  const [data, q] = await Promise.all([AsyncStorage.getItem(DATA_KEY), AsyncStorage.getItem(QUEUE_KEY)]);
  if (q) queue = JSON.parse(q);
  const parsed = data ? JSON.parse(data) : { tasks: [], projects: [] };
  setState({ tasks: parsed.tasks, projects: parsed.projects, hydrated: true });
  void sync();
}

// ---- mutations (instant; network happens later) ----

function now() {
  return new Date().toISOString();
}

// Fields the API accepts — everything else (timestamps, completed_at) is local
// bookkeeping until the server's authoritative row arrives via pull().
const TASK_FIELDS = ["title", "notes", "status", "project_id", "context", "waiting_for", "due_date", "defer_date", "priority", "estimate_min", "sort_order"] as const;
const PROJECT_FIELDS = ["name", "status", "notes", "sort_order"] as const;

function pickFields<T>(data: Partial<T>, fields: readonly (keyof T & string)[]): Partial<T> {
  const out: Partial<T> = {};
  for (const f of fields) if (f in data) out[f] = data[f];
  return out;
}

function enqueue(op: Op) {
  // Coalesce consecutive updates to the same row into one op.
  const last = queue[queue.length - 1];
  if (last && op.kind === last.kind && op.kind.endsWith("update") && last.id === op.id) {
    last.data = { ...last.data, ...op.data };
  } else {
    queue.push(op);
  }
  persistQueue();
  setState({});
  void sync();
}

export function createTask(data: Partial<Task> & { title: string }): Task {
  const task: Task = {
    id: Crypto.randomUUID(),
    title: data.title,
    notes: data.notes ?? "",
    status: data.status ?? "inbox",
    project_id: data.project_id ?? null,
    context: data.context ?? null,
    waiting_for: data.waiting_for ?? null,
    due_date: data.due_date ?? null,
    defer_date: data.defer_date ?? null,
    priority: data.priority ?? 4,
    estimate_min: data.estimate_min ?? null,
    completed_at: null,
    sort_order: data.sort_order ?? 0,
    created_at: now(),
    updated_at: now(),
  };
  setState({ tasks: [task, ...state.tasks] });
  persistData();
  enqueue({ kind: "task.create", id: task.id, data: pickFields(task, TASK_FIELDS) });
  return task;
}

export function updateTask(id: string, patch: Partial<Task>) {
  setState({
    tasks: state.tasks.map((t) => {
      if (t.id !== id) return t;
      const next = { ...t, ...patch, updated_at: now() };
      // Mirror the server's completed_at rule so the UI is right pre-sync.
      next.completed_at = next.status === "done" ? (t.completed_at ?? now()) : null;
      return next;
    }),
  });
  persistData();
  enqueue({ kind: "task.update", id, data: pickFields(patch, TASK_FIELDS) });
}

export function trashTask(id: string) {
  updateTask(id, { status: "trashed" });
}

export function createProject(data: Partial<Project> & { name: string }): Project {
  const project: Project = {
    id: Crypto.randomUUID(),
    name: data.name,
    status: data.status ?? "active",
    notes: data.notes ?? "",
    sort_order: data.sort_order ?? 0,
    created_at: now(),
    updated_at: now(),
  };
  setState({ projects: [project, ...state.projects] });
  persistData();
  enqueue({ kind: "project.create", id: project.id, data: pickFields(project, PROJECT_FIELDS) });
  return project;
}

export function updateProject(id: string, patch: Partial<Project>) {
  setState({ projects: state.projects.map((p) => (p.id === id ? { ...p, ...patch, updated_at: now() } : p)) });
  persistData();
  enqueue({ kind: "project.update", id, data: pickFields(patch, PROJECT_FIELDS) });
}

export function dropProject(id: string) {
  updateProject(id, { status: "dropped" });
}

// ---- sync ----

let syncRunning = false;
let syncAgain = false;

export async function sync() {
  if (syncRunning) {
    syncAgain = true;
    return;
  }
  const { token } = await getConfig();
  if (!token) return;
  syncRunning = true;
  setState({ syncing: true });
  try {
    while (queue.length) {
      await push(queue[0]);
      queue.shift();
      persistQueue();
      setState({});
    }
    await pull();
    setState({ syncError: null });
  } catch (e) {
    setState({ syncError: e instanceof Error ? e.message : String(e) });
  } finally {
    syncRunning = false;
    setState({ syncing: false });
    if (syncAgain) {
      syncAgain = false;
      void sync();
    }
  }
}

async function push(op: Op) {
  try {
    if (op.kind === "task.create") await api.tasks.create({ id: op.id, ...op.data } as never);
    else if (op.kind === "task.update") await api.tasks.update(op.id, op.data as Partial<Task>);
    else if (op.kind === "project.create") await api.projects.create({ id: op.id, ...op.data } as never);
    else await api.projects.update(op.id, op.data as Partial<Project>);
  } catch (e) {
    // 4xx (bad data, row gone) would poison the queue forever — drop the op.
    // Anything else (offline, 5xx) rethrows and we retry on the next sync.
    const status = (e as { status?: number }).status;
    if (status && status >= 400 && status < 500 && status !== 401) return;
    throw e;
  }
}

async function pull() {
  const [tasks, projects] = await Promise.all([api.tasks.list(), api.projects.list()]);
  // Rows with ops still queued keep their local version; the next sync pass
  // pushes those ops and then this merge converges to the server state.
  const dirty = new Set(queue.map((o) => o.id));
  const keepLocal = <T extends { id: string }>(local: T[], server: T[]) => {
    const serverIds = new Set(server.map((r) => r.id));
    return [
      ...server.map((r) => (dirty.has(r.id) ? local.find((l) => l.id === r.id) ?? r : r)),
      ...local.filter((l) => dirty.has(l.id) && !serverIds.has(l.id)),
    ];
  };
  setState({
    tasks: keepLocal(state.tasks, tasks),
    projects: keepLocal(state.projects, projects),
  });
  persistData();
}

// ---- selectors ----

function byPriorityDue(a: Task, b: Task): number {
  return (
    a.priority - b.priority ||
    (a.due_date ?? "9999").localeCompare(b.due_date ?? "9999") ||
    a.sort_order - b.sort_order ||
    b.created_at.localeCompare(a.created_at)
  );
}

export function tasksByStatus(s: State, status: Task["status"]): Task[] {
  return s.tasks.filter((t) => t.status === status).sort(byPriorityDue);
}

/** Overdue + due-today + deferred-arriving-today, across all open lists. */
export function todayTasks(s: State): { overdue: Task[]; today: Task[] } {
  const now = new Date();
  const todayIso = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
  const open = s.tasks.filter((t) => t.status !== "done" && t.status !== "trashed");
  return {
    overdue: open.filter((t) => t.due_date && t.due_date < todayIso).sort(byPriorityDue),
    today: open
      .filter((t) => t.due_date === todayIso || (t.defer_date === todayIso && !t.due_date))
      .sort(byPriorityDue),
  };
}

export function activeProjects(s: State): Project[] {
  return s.projects
    .filter((p) => p.status !== "dropped")
    .sort((a, b) => a.sort_order - b.sort_order || b.created_at.localeCompare(a.created_at));
}

export function openTaskCount(s: State, projectId: string): number {
  return s.tasks.filter((t) => t.project_id === projectId && t.status !== "done" && t.status !== "trashed").length;
}
