import { randomBytes } from "node:crypto";
import { CollectorFatalError } from "../errors";
import { KanboardRpcError, type KanboardRpcClient, type KanboardResult } from "./kanboard-client";

export const INCIDENTS_PROJECT_IDENTIFIER = "OVERDECKINCIDENTS";
export const INCIDENTS_PROJECT_NAME = "Overdeck Incidents";
const STOCK_COLUMNS = ["Backlog", "Ready", "Work in progress", "Done"] as const;
const STOCK_SWIMLANE = "Default swimlane";

export const INCIDENTS_COLUMNS = [
  "Filed",
  "Dispatching",
  "Running",
  "Needs attention",
  "Resolved",
] as const;

export const INCIDENTS_SWIMLANES = [
  "P0 · Critical",
  "P1 · High",
  "P2 · Normal",
  "P3 · Low",
] as const;

export const INCIDENTS_AGENT_USERNAME = "overdeck-agent";

type IncidentColumnName = (typeof INCIDENTS_COLUMNS)[number];
type IncidentSwimlaneName = (typeof INCIDENTS_SWIMLANES)[number];

export type IncidentsReadinessStatus = "ready" | "degraded";

export type IncidentsColumnIds = Record<IncidentColumnName, number>;
export type IncidentsSwimlaneIds = Record<IncidentSwimlaneName, number>;

export interface IncidentsReadiness {
  status: IncidentsReadinessStatus;
  drift: string[];
  projectId: number;
  columnIds: IncidentsColumnIds;
  swimlaneIds: IncidentsSwimlaneIds;
  agentUserId: number;
}

type ProjectRecord = KanboardResult<"getProjectByIdentifier">;
type ColumnRecord = KanboardResult<"getColumns">[number];
type SwimlaneRecord = KanboardResult<"getAllSwimlanes">[number];

/** The attribution user never authenticates; its id is only a createComment author. */
function unusableAgentPassword(): string {
  return randomBytes(32).toString("hex");
}

function isActiveSwimlane(swimlane: SwimlaneRecord): boolean {
  return swimlane.is_active === true;
}

function toInt(value: unknown): number | undefined {
  if (typeof value === "number" && Number.isFinite(value)) {
    return value;
  }
  if (typeof value === "string" && /^-?\d+$/.test(value)) {
    return Number(value);
  }
  return undefined;
}

function pushUnique(set: Set<string>, value: string): void {
  set.add(value);
}

function isAbsenceResult(error: unknown): boolean {
  return (
    error instanceof KanboardRpcError &&
    (error.code === "RPC_FALSE_RESULT" || error.code === "RPC_NULL_RESULT")
  );
}

async function getProjectByIdentifier(
  client: KanboardRpcClient,
): Promise<ProjectRecord | null> {
  try {
    return await client.call("getProjectByIdentifier", {
      identifier: INCIDENTS_PROJECT_IDENTIFIER,
    });
  } catch (error) {
    if (isAbsenceResult(error)) {
      return null;
    }
    throw error;
  }
}

function projectPayloadFromRecord(project: ProjectRecord): {
  name: string;
  is_public: boolean;
  priority_start: number;
  priority_end: number;
  priority_default: number;
} {
  return {
    name: project.name,
    is_public: Boolean(project.is_public),
    priority_start: toInt(project.priority_start) ?? 0,
    priority_end: toInt(project.priority_end) ?? 3,
    priority_default: toInt(project.priority_default) ?? 2,
  };
}

async function ensureProject(client: KanboardRpcClient, drift: Set<string>): Promise<number> {
  const existing = await getProjectByIdentifier(client);
  if (!existing) {
    const createdProjectId = await client.call("createProject", {
      name: INCIDENTS_PROJECT_NAME,
      identifier: INCIDENTS_PROJECT_IDENTIFIER,
      priority_start: 0,
      priority_end: 3,
      priority_default: 2,
    });
    return createdProjectId;
  }

  const current = projectPayloadFromRecord(existing);
  const update: {
    project_id: number;
    name?: string;
    priority_start?: number;
    priority_end?: number;
    priority_default?: number;
  } = { project_id: existing.id };
  if (current.name !== INCIDENTS_PROJECT_NAME) update.name = INCIDENTS_PROJECT_NAME;
  if (current.priority_start !== 0) update.priority_start = 0;
  if (current.priority_end !== 3) update.priority_end = 3;
  if (current.priority_default !== 2) update.priority_default = 2;
  if (current.is_public) {
    pushUnique(drift, "project:is_public");
  }

  if (Object.keys(update).length > 1) {
    await client.call("updateProject", update);
  }
  return existing.id;
}

async function ensureColumns(
  client: KanboardRpcClient,
  projectId: number,
  drift: Set<string>,
): Promise<IncidentsColumnIds> {
  const columns = await client.call("getColumns", { project_id: projectId });
  const byName = new Map<string, ColumnRecord>();
  for (const column of columns) {
    byName.set(column.title, column);
  }

  for (const column of columns) {
    if (
      !STOCK_COLUMNS.includes(column.title as (typeof STOCK_COLUMNS)[number])
      && !INCIDENTS_COLUMNS.includes(column.title as IncidentColumnName)
    ) {
      pushUnique(drift, `column:${column.title}`);
    }
  }

  const orderedColumns: ColumnRecord[] = [];
  const usedIds = new Set<number>();
  const stockColumns = STOCK_COLUMNS.map((title) => byName.get(title))
    .filter((column): column is ColumnRecord => Boolean(column))
    .sort((left, right) => {
      const leftPosition = toInt(left.position) ?? 0;
      const rightPosition = toInt(right.position) ?? 0;
      return leftPosition - rightPosition;
    });

  if (stockColumns.length === STOCK_COLUMNS.length) {
    for (let index = 0; index < STOCK_COLUMNS.length; index += 1) {
      const stockColumn = stockColumns[index];
      const title = INCIDENTS_COLUMNS[index];
      if (!stockColumn || !title) {
        continue;
      }
      if (stockColumn.title !== title) {
        await client.call("updateColumn", { column_id: stockColumn.id, title });
        stockColumn.title = title;
      }
      orderedColumns.push(stockColumn);
      usedIds.add(stockColumn.id);
    }
  }

  for (let index = 0; index < INCIDENTS_COLUMNS.length; index += 1) {
    const title = INCIDENTS_COLUMNS[index];
    if (!title) {
      throw new CollectorFatalError(
        "INCIDENTS_BOOTSTRAP_DERIVATION",
        "failed to map required incident columns",
      );
    }
    if (stockColumns.length === STOCK_COLUMNS.length && index < STOCK_COLUMNS.length) {
      continue;
    }
    const found = byName.get(title);
    if (found && !usedIds.has(found.id)) {
      orderedColumns.push(found);
      usedIds.add(found.id);
      continue;
    }

    const createdColumnId = await client.call("addColumn", {
      project_id: projectId,
      title,
      position: index,
    });
    orderedColumns.push({
      id: createdColumnId,
      title,
      position: index,
    });
    usedIds.add(createdColumnId);
  }

  for (let index = 0; index < INCIDENTS_COLUMNS.length; index += 1) {
    const column = orderedColumns[index];
    if (!column) {
      throw new CollectorFatalError(
        "INCIDENTS_BOOTSTRAP_DERIVATION",
        "failed to materialize all required columns",
      );
    }
    const targetPosition = index + 1;
    const position = toInt(column.position) ?? -1;
    if (position !== targetPosition) {
      await client.call("changeColumnPosition", { project_id: projectId, column_id: column.id, position: targetPosition });
    }
  }

  const columnsByName = {} as IncidentsColumnIds;
  for (let i = 0; i < INCIDENTS_COLUMNS.length; i += 1) {
    const title = INCIDENTS_COLUMNS[i];
    const column = orderedColumns[i];
    if (!title || !column) {
      throw new CollectorFatalError(
        "INCIDENTS_BOOTSTRAP_DERIVATION",
        "failed to materialize all required columns",
      );
    }
    columnsByName[title] = column.id;
  }
  return columnsByName;

}

async function ensureSwimlanes(
  client: KanboardRpcClient,
  projectId: number,
  drift: Set<string>,
): Promise<IncidentsSwimlaneIds> {
  const swimlanes = await client.call("getAllSwimlanes", { project_id: projectId });
  const byName = new Map<string, SwimlaneRecord>();
  for (const swimlane of swimlanes) {
    byName.set(swimlane.name, swimlane);
  }

  for (const swimlane of swimlanes) {
    if (swimlane.name === STOCK_SWIMLANE) continue;
    if (!INCIDENTS_SWIMLANES.includes(swimlane.name as IncidentSwimlaneName)) {
      pushUnique(drift, `swimlane:${swimlane.name}`);
    }
  }

  const orderedSwimlanes: SwimlaneRecord[] = [];
  const usedSwimlaneIds = new Set<number>();

  for (const name of INCIDENTS_SWIMLANES) {
    const found = byName.get(name);
    if (found && !usedSwimlaneIds.has(found.id)) {
      orderedSwimlanes.push(found);
      usedSwimlaneIds.add(found.id);
      continue;
    }
    const createdSwimlaneId = await client.call("addSwimlane", { project_id: projectId, name });
    orderedSwimlanes.push({
      id: createdSwimlaneId,
      name,
      position: INCIDENTS_SWIMLANES.indexOf(name),
      is_active: true,
    });
    usedSwimlaneIds.add(createdSwimlaneId);
  }

  for (const swimlane of orderedSwimlanes) {
    if (!isActiveSwimlane(swimlane)) {
      await client.call("enableSwimlane", { project_id: projectId, swimlane_id: swimlane.id });
    }
  }

  const defaultSwimlane = byName.get(STOCK_SWIMLANE);
  const defaultPosition = defaultSwimlane ? toInt(defaultSwimlane.position) ?? 0 : 0;

  for (let index = 0; index < INCIDENTS_SWIMLANES.length; index += 1) {
    const swimlane = orderedSwimlanes[index];
    if (!swimlane) continue;
    const targetPosition = defaultPosition + 1 + index;
    if ((toInt(swimlane.position) ?? -1) !== targetPosition) {
      await client.call("changeSwimlanePosition", { project_id: projectId, swimlane_id: swimlane.id, position: targetPosition });
    }
  }

  const swimlaneByName = {} as IncidentsSwimlaneIds;
  for (let i = 0; i < INCIDENTS_SWIMLANES.length; i += 1) {
    const swimlane = orderedSwimlanes[i];
    const name = INCIDENTS_SWIMLANES[i];
    if (!name || !swimlane) {
      throw new CollectorFatalError(
        "INCIDENTS_BOOTSTRAP_DERIVATION",
        "failed to materialize all required swimlanes",
      );
    }
    swimlaneByName[name] = swimlane.id;
  }
  return swimlaneByName;
}

async function ensureAttributionUser(client: KanboardRpcClient): Promise<number> {
  try {
    const user = await client.call("getUserByName", { username: INCIDENTS_AGENT_USERNAME });
    return user.id;
  } catch (error) {
    if (!isAbsenceResult(error)) {
      throw error;
    }
  }

  const createdUserId = await client.call("createUser", {
    username: INCIDENTS_AGENT_USERNAME,
    password: unusableAgentPassword(),
    name: "Overdeck Agent",
  });
  return createdUserId;
}

export async function checkIncidentsReadiness(
  client: KanboardRpcClient,
): Promise<IncidentsReadiness> {
  const drift = new Set<string>();
  const project = await ensureProject(client, drift);
  const columnIds = await ensureColumns(client, project, drift);
  const swimlaneIds = await ensureSwimlanes(client, project, drift);
  const agentUserId = await ensureAttributionUser(client);

  return {
    status: drift.size === 0 ? "ready" : "degraded",
    drift: [...drift].sort(),
    projectId: project,
    columnIds,
    swimlaneIds,
    agentUserId,
  };
}
