import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { isItemPriority } from "../db/pipelineItem";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  ItemClosedError,
  ItemNotFoundError,
  InvalidPriorityError,
  InvalidTitleError,
  UserNotFoundError,
} from "../lib/errors.generated";

export interface UpdatePipelineItemDeps {
  /**
   * Optional assignee-existence check, injected via
   * defineModule({ userManagement: { getUser } }). When omitted, assigneeId
   * is stored as-is.
   */
  getUser?: (id: string) => Promise<{ id: string } | null>;
}

export interface UpdatePipelineItemInput {
  itemId: string;
  title?: string;
  description?: string | null;
  assigneeId?: string | null;
  priority?: string;
  dueDate?: Date | string | null;
  position?: number;
}

// dueDate is exposed in multiple shapes (Date object, ISO string, null). The
// audit log stores a single canonical string form so consumers can interpret
// it uniformly, and so a no-op write (same string) doesn't generate a row.
const dueDateToAuditString = (value: Date | string | null | undefined): string | null => {
  if (value === null || value === undefined) return null;
  if (value instanceof Date) return value.toISOString();
  const d = new Date(value);
  if (Number.isNaN(d.getTime())) return value;
  return d.toISOString();
};

const ITEM_OWN_KEYS = new Set([
  "itemId",
  "title",
  "description",
  "assigneeId",
  "priority",
  "dueDate",
  "position",
]);

// Module-managed PipelineItem columns that must never be writable through the
// extension-field pass-through — lifecycle/stage changes go through their
// dedicated commands (close/open/reopen/move), and itemNumber is immutable.
const ITEM_RESERVED_KEYS = new Set([
  "id",
  "pipelineId",
  "stageId",
  "itemNumber",
  "lifecycle",
  "createdAt",
  "updatedAt",
]);

export async function run<CF extends Record<string, unknown>>(
  db: Transaction,
  input: UpdatePipelineItemInput & Partial<CF>,
  ctx: CommandContext,
  deps: UpdatePipelineItemDeps = {},
) {
  // Consumer extension fields (declared via defineModule's pipelineItem.fields)
  // pass through to the update untouched — except reserved model columns.
  const customFields: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
    if (!ITEM_OWN_KEYS.has(key) && !ITEM_RESERVED_KEYS.has(key)) {
      customFields[key] = value;
    }
  }

  const item = await db
    .selectFrom("PipelineItem")
    .selectAll()
    .where("id", "=", input.itemId)
    .forUpdate()
    .executeTakeFirst();

  if (!item) {
    return err(new ItemNotFoundError(input.itemId));
  }

  if (item.lifecycle === "CLOSED") {
    return err(new ItemClosedError(input.itemId));
  }

  if (input.title !== undefined && !input.title.trim()) {
    return err(new InvalidTitleError(input.title));
  }

  if (input.priority !== undefined && !isItemPriority(input.priority)) {
    return err(new InvalidPriorityError(input.priority));
  }

  // null clears the assignee and needs no existence check.
  if (deps.getUser && input.assigneeId !== undefined && input.assigneeId !== null) {
    const assignee = await deps.getUser(input.assigneeId);
    if (!assignee) {
      return err(new UserNotFoundError(input.assigneeId));
    }
  }

  const update: Record<string, unknown> = {
    ...customFields,
    updatedAt: new Date(),
  };

  if (input.title !== undefined) update.title = input.title;
  if (input.description !== undefined) update.description = input.description;
  if (input.assigneeId !== undefined) update.assigneeId = input.assigneeId;
  if (input.priority !== undefined) update.priority = input.priority;
  if (input.dueDate !== undefined) update.dueDate = input.dueDate;
  if (input.position !== undefined) update.position = input.position;

  const titleChanged = input.title !== undefined && input.title !== item.title;
  const descriptionChanged =
    input.description !== undefined && (input.description ?? null) !== (item.description ?? null);
  const assigneeChanged =
    input.assigneeId !== undefined && (input.assigneeId ?? null) !== (item.assigneeId ?? null);
  const priorityChanged = input.priority !== undefined && input.priority !== item.priority;
  const prevDueDate = dueDateToAuditString(item.dueDate);
  const newDueDate =
    input.dueDate !== undefined ? dueDateToAuditString(input.dueDate) : prevDueDate;
  const dueDateChanged = input.dueDate !== undefined && prevDueDate !== newDueDate;

  const updatedItem = await db
    .updateTable("PipelineItem")
    .set(update)
    .where("id", "=", input.itemId)
    .returningAll()
    .executeTakeFirst();

  const now = new Date();

  const changes: Array<{
    changeType: "CONTENT" | "FIELD";
    fieldName: "TITLE" | "DESCRIPTION" | "ASSIGNEE" | "PRIORITY" | "DUE_DATE";
    prevValue: string | null;
    newValue: string | null;
  }> = [];

  if (titleChanged && input.title !== undefined) {
    changes.push({
      changeType: "CONTENT",
      fieldName: "TITLE",
      prevValue: item.title,
      newValue: input.title,
    });
  }

  if (descriptionChanged) {
    changes.push({
      changeType: "CONTENT",
      fieldName: "DESCRIPTION",
      prevValue: item.description ?? null,
      newValue: input.description ?? null,
    });
  }

  if (assigneeChanged) {
    changes.push({
      changeType: "FIELD",
      fieldName: "ASSIGNEE",
      prevValue: item.assigneeId ?? null,
      newValue: input.assigneeId ?? null,
    });
  }

  if (priorityChanged && input.priority !== undefined) {
    changes.push({
      changeType: "FIELD",
      fieldName: "PRIORITY",
      prevValue: item.priority,
      newValue: input.priority,
    });
  }

  if (dueDateChanged) {
    changes.push({
      changeType: "FIELD",
      fieldName: "DUE_DATE",
      prevValue: prevDueDate,
      newValue: newDueDate,
    });
  }

  if (changes.length > 0) {
    await db
      .insertInto("PipelineItemChange")
      .values(
        changes.map((c) => ({
          itemId: item.id,
          changedByUserId: ctx.actorId,
          changeType: c.changeType,
          fieldName: c.fieldName,
          prevValue: c.prevValue,
          newValue: c.newValue,
          createdAt: now,
          updatedAt: null,
        })),
      )
      .execute();
  }

  return ok({ item: updatedItem ?? { ...item, ...update } });
}
