import type {
  MeetingSummaryRow,
  MeetingSummaryStatus,
} from "./meeting-summary-data";
import type {
  MeetingSummaryParticipants,
  ViewMeetingSummaryDetail,
  ViewMeetingSummaryIssueRow,
} from "./components/view-meeting-summary/view-meeting-summary-data";
import { resolveAgencyLogoForDisplay } from "./components/create-meeting-summary/meeting-summary-issue-agencies";
import type { Agency } from "@/features/ministry/dashboard/components/add-dashboard-issue-dialog/add-dashboard-issue-dialog-types";
import { formatDate } from "@/lib/date-utils";
import { getDisplayFileName } from "@/lib/document-file";

const API_BASE_URL =
  process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api/v1";

type ApiResponse<T> = {
  success: boolean;
  statusCode: number;
  message: string;
  data: T;
};

// Shape of one row returned by GET /meeting-summaries.
type ApiMeetingSummaryRow = {
  id: number;
  summaryTitle?: string | null;
  meetingDate?: string | null;
  issueCount?: number | null;
  meetingRequest?: string | null;
  meetingSummary?: string | null;
  governmentAgency?: string | null;
  governmentAgencyLogo?: string | null;
  pswg?: string | null;
  status?: string | null;
  meetingPswg?: string | null;
};

type ApiMeetingSummaryListResponse = {
  items: ApiMeetingSummaryRow[];
  meta?: { page: number; limit: number; total: number; totalPages: number };
};

export type CreateMeetingSummaryInput = {
  meetingId: number;
  file?: File | null;
  status?: "DRAFT" | "SUBMITTED";
  issues?: ViewMeetingSummaryIssueRow[];
  participants?: MeetingSummaryParticipants;
};

export type UpdateMeetingSummaryInput = {
  file?: File | null;
  status?: MeetingSummaryStatus;
  issues?: ViewMeetingSummaryIssueRow[];
  participants?: MeetingSummaryParticipants;
};

export type MeetingSummaryIssueResolvePayload = {
  issueId: number;
  status?: string;
  escalate?: string | null;
  rgcDecision?: string;
  nextStep?: string;
  remark?: string;
  agencyStakeholderIds?: number[];
};

export type UpsertMeetingSummaryIssueInput = {
  status?: string;
  issueEscalation?: string;
  rgcDecision?: string;
  nextStep?: string;
  remark?: string;
  attachment?: File | null;
  agencyStakeholderIds?: number[];
};

export type IssueStatusLookup = {
  id: number;
  code: string;
  name: string;
};

export async function getIssueStatuses(): Promise<IssueStatusLookup[]> {
  const data = await apiFetch<IssueStatusLookup[]>("/working-group-issues/statuses");
  return Array.isArray(data) ? data : [];
}

export function mergeIssueRowAfterSave(
  previous: ViewMeetingSummaryIssueRow,
  edited: ViewMeetingSummaryIssueRow,
  saved?: ViewMeetingSummaryIssueRow,
): ViewMeetingSummaryIssueRow {
  const merged: ViewMeetingSummaryIssueRow = {
    ...previous,
    ...saved,
    ...edited,
    id: previous.id,
    wgName: previous.wgName,
    issue: previous.issue,
    category: previous.category,
    issueDescription: previous.issueDescription,
    recommendation: previous.recommendation,
    primaryAgency: previous.primaryAgency,
    primaryAgencyLogo: resolveAgencyLogoForDisplay(
      previous.primaryAgency,
      edited.primaryAgencyLogo ??
        saved?.primaryAgencyLogo ??
        previous.primaryAgencyLogo,
    ),
    issueReference:
      edited.issueReference !== "Not Uploaded"
        ? edited.issueReference
        : saved?.issueReference ?? previous.issueReference,
    secondAgencyLogo: resolveAgencyLogoForDisplay(
      edited.secondAgency ?? saved?.secondAgency ?? previous.secondAgency,
      edited.secondAgencyLogo ??
        saved?.secondAgencyLogo ??
        previous.secondAgencyLogo,
    ),
    thirdAgencyLogo: resolveAgencyLogoForDisplay(
      edited.thirdAgency ?? saved?.thirdAgency ?? previous.thirdAgency,
      edited.thirdAgencyLogo ??
        saved?.thirdAgencyLogo ??
        previous.thirdAgencyLogo,
    ),
    fourthAgencyLogo: resolveAgencyLogoForDisplay(
      edited.fourthAgency ?? saved?.fourthAgency ?? previous.fourthAgency,
      edited.fourthAgencyLogo ??
        saved?.fourthAgencyLogo ??
        previous.fourthAgencyLogo,
    ),
    fifthAgencyLogo: resolveAgencyLogoForDisplay(
      edited.fifthAgency ?? saved?.fifthAgency ?? previous.fifthAgency,
      edited.fifthAgencyLogo ??
        saved?.fifthAgencyLogo ??
        previous.fifthAgencyLogo,
    ),
    agencies: edited.agencies ?? saved?.agencies ?? previous.agencies,
  };

  return merged;
}

function getAccessToken() {
  if (typeof window === "undefined") return "";

  return (
    localStorage.getItem("accessToken") ||
    localStorage.getItem("token") ||
    localStorage.getItem("authToken") ||
    ""
  );
}

async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
  const token = getAccessToken();
  const hasBody = Boolean(options.body);
  // The browser sets the correct multipart boundary for FormData itself, so we
  // only add a JSON Content-Type for plain (string) bodies.
  const isFormData =
    typeof FormData !== "undefined" && options.body instanceof FormData;

  const response = await fetch(`${API_BASE_URL}${path}`, {
    ...options,
    cache: "no-store",
    credentials: "include",
    headers: {
      Accept: "application/json",
      ...(hasBody && !isFormData ? { "Content-Type": "application/json" } : {}),
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options.headers,
    },
  });

  const json = (await response
    .json()
    .catch(() => null)) as ApiResponse<T> | null;

  if (!response.ok) {
    const validationErrors = (
      json as ApiResponse<T> & { errors?: string[] } | null
    )?.errors;
    const detail =
      validationErrors?.length && validationErrors.length > 0
        ? validationErrors.join(", ")
        : json?.message || `API error ${response.status}`;

    throw new Error(detail);
  }

  return json?.data as T;
}

function getApiOrigin() {
  return API_BASE_URL.replace(/\/api\/v\d+\/?$/, "").replace(/\/+$/, "");
}

function getMediaUrl(path?: string | null) {
  if (!path) return null;
  if (/^https?:\/\//i.test(path)) return path;
  return `${getApiOrigin()}${path.startsWith("/") ? path : `/${path}`}`;
}

function getFileName(value?: string | null) {
  if (!value) return "";
  return getDisplayFileName(value, "");
}

// Only the four labels the table understands; fall back to "DRAFT".
function normalizeStatus(value?: string | null): MeetingSummaryStatus {
  if (
    value === "DRAFT" ||
    value === "SUBMITTED" ||
    value === "COMPLETED" ||
    value === "REVIEWED"
  ) {
    return value;
  }
  return "DRAFT";
}

function mapApiRowToMeetingSummaryRow(
  row: ApiMeetingSummaryRow,
): MeetingSummaryRow {
  return {
    id: row.id,
    summaryTitle: row.summaryTitle?.trim() || "-",
    meetingDate: row.meetingDate || "-",
    issueCount: row.issueCount ?? 0,
    meetingRequest: row.meetingRequest?.trim() || "",
    meetingSummary: row.meetingSummary?.trim() || "",
    governmentAgency: row.governmentAgency?.trim() || "-",
    governmentAgencyLogo: getMediaUrl(row.governmentAgencyLogo),
    pswg: row.pswg?.trim() || "-",
    status: normalizeStatus(row.status),
    meetingPswg: row.meetingPswg?.trim() || "-",
  };
}

export async function getMeetingSummaries(): Promise<MeetingSummaryRow[]> {
  const data = await apiFetch<ApiMeetingSummaryListResponse>(
    "/meeting-summaries",
  );

  return data.items.map(mapApiRowToMeetingSummaryRow);
}

// Pull the filename out of a Content-Disposition header, with a safe default.
function getExportFilename(contentDisposition: string | null) {
  if (!contentDisposition) return "meeting-summaries.xlsx";

  const match = /filename="([^"]+)"/i.exec(contentDisposition);
  return match?.[1] ?? "meeting-summaries.xlsx";
}

// Ask the backend for an Excel file of the meeting summaries and hand it to
// the browser as a normal file download.
export async function exportMeetingSummaries(): Promise<void> {
  const token = getAccessToken();

  const response = await fetch(`${API_BASE_URL}/meeting-summaries/export`, {
    method: "GET",
    cache: "no-store",
    credentials: "include",
    headers: token ? { Authorization: `Bearer ${token}` } : {},
  });

  if (!response.ok) {
    throw new Error(`Export failed (${response.status})`);
  }

  const blob = await response.blob();
  const filename = getExportFilename(
    response.headers.get("Content-Disposition"),
  );

  // A temporary link lets the browser save the file with the right name.
  const objectUrl = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = objectUrl;
  anchor.download = filename;
  anchor.click();
  URL.revokeObjectURL(objectUrl);
}

type ApiAgencyOption = {
  id: number;
  name?: string | null;
  logo?: string | null;
};

// Real government agencies (with real stakeholder ids) for the issue-resolution
// agency dropdowns. The select uses the stakeholder id as its value (as a
// string), so the chosen agency can be persisted by id.
export async function getMeetingSummaryAgencies(): Promise<Agency[]> {
  const data = await apiFetch<ApiAgencyOption[]>("/meeting-summaries/agencies");

  return (Array.isArray(data) ? data : []).map((agency) => ({
    id: String(agency.id),
    name: agency.name?.trim() || "Unknown Agency",
    logo: getMediaUrl(agency.logo) || "",
  }));
}

export async function createMeetingSummary(
  input: CreateMeetingSummaryInput,
): Promise<number> {
  const status = input.status ?? "DRAFT";
  const issueResolves = buildIssueResolvesFromRows(input.issues ?? []);

  // When a reference PDF is selected we send multipart so the file is uploaded;
  // otherwise a small JSON body is enough to create the draft.
  if (input.file) {
    const formData = new FormData();
    formData.append("meetingId", String(input.meetingId));
    formData.append("status", status);
    formData.append("documentReference", input.file);
    appendParticipantsToFormData(formData, input.participants);
    appendIssueResolvesToFormData(formData, issueResolves);

    const data = await apiFetch<ApiMeetingSummaryMutationResponse>(
      "/meeting-summaries",
      {
        method: "POST",
        body: formData,
      },
    );

    return extractSummaryId(data);
  }

  const data = await apiFetch<ApiMeetingSummaryMutationResponse>(
    "/meeting-summaries",
    {
      method: "POST",
      body: JSON.stringify({
        meetingId: input.meetingId,
        status,
        ...(input.participants ?? {}),
        ...(issueResolves.length > 0 ? { issueResolves } : {}),
      }),
    },
  );

  return extractSummaryId(data);
}

export async function updateMeetingSummary(
  id: number,
  input: UpdateMeetingSummaryInput,
): Promise<void> {
  const status = input.status;
  const issueResolves = buildIssueResolvesFromRows(input.issues ?? []);
  const hasIssueResolves = issueResolves.length > 0;

  if (input.file) {
    const formData = new FormData();
    if (status) formData.append("status", status);
    formData.append("documentReference", input.file);
    appendParticipantsToFormData(formData, input.participants);
    appendIssueResolvesToFormData(formData, issueResolves);

    await apiFetch(`/meeting-summaries/${id}`, {
      method: "PATCH",
      body: formData,
    });
    return;
  }

  if (status || hasIssueResolves || input.participants) {
    await apiFetch(`/meeting-summaries/${id}`, {
      method: "PATCH",
      body: JSON.stringify({
        ...(status ? { status } : {}),
        ...(input.participants ?? {}),
        ...(hasIssueResolves ? { issueResolves } : {}),
      }),
    });
  }
}

// Change a meeting summary's status by code (e.g. PSWG "Mark as Reviewed").
export async function changeMeetingSummaryStatus(
  id: number,
  status: MeetingSummaryStatus,
): Promise<void> {
  await apiFetch(`/meeting-summaries/${id}/status`, {
    method: "PATCH",
    body: JSON.stringify({ status }),
  });
}

// Share a meeting summary with its PSWG working group so private-sector users
// can see it on /pswg/meeting-summary.
export async function shareMeetingSummaryWithPswg(id: number): Promise<void> {
  await apiFetch(`/meeting-summaries/${id}/share`, {
    method: "PATCH",
  });
}

// Reverse a share — hide the summary from the PSWG again.
export async function unshareMeetingSummaryWithPswg(id: number): Promise<void> {
  await apiFetch(`/meeting-summaries/${id}/unshare`, {
    method: "PATCH",
  });
}

// --- single meeting-summary detail (the view page) --------------------------

type ApiSummaryIssue = {
  id: number;
  issue?: string | null;
  category?: string | null;
  issueDescription?: string | null;
  recommendation?: string | null;
  issueReference?: string | null;
  primaryAgency?: string | null;
  primaryAgencyLogo?: string | null;
  secondAgency?: string | null;
  secondAgencyLogo?: string | null;
  thirdAgency?: string | null;
  thirdAgencyLogo?: string | null;
  fourthAgency?: string | null;
  fourthAgencyLogo?: string | null;
  fifthAgency?: string | null;
  fifthAgencyLogo?: string | null;
  status?: string | null;
  escalation?: string | null;
  rgcDecision?: unknown;
  nextStep?: unknown;
  remark?: unknown;
  referenceDocument?: string | null;
  agencies?: { id: number; name?: string | null; logo?: string | null }[] | null;
};

type ApiSummaryDetail = {
  id: number;
  documentReference?: string | null;
  status?: string | null;
  share?: boolean | null;
  pswgReporter?: string | null;
  pswgReporterPosition?: string | null;
  pswgRepresentative?: string | null;
  pswgRepresentativePosition?: string | null;
  ministryReporter?: string | null;
  ministryReporterPosition?: string | null;
  ministryRepresentative?: string | null;
  ministryRepresentativePosition?: string | null;
  meeting?: {
    title?: string | null;
    meetingDate?: string | null;
    startTime?: string | null;
    endTime?: string | null;
    location?: string | null;
    documentReference?: string | null;
  } | null;
  meetingRequest?: {
    title?: string | null;
    meetingRequestLetter?: string | null;
    submittedBy?: string | null;
    governmentAgency?: string | null;
    governmentAgencyLogo?: string | null;
    pswg?: string | null;
  } | null;
  issues?: ApiSummaryIssue[] | null;
};

type ApiMeetingSummaryMutationResponse = {
  message?: string;
  meetingSummary: ApiSummaryDetail;
};

function normalizeSummaryStatus(
  value?: string | null,
): ViewMeetingSummaryDetail["status"] {
  if (
    value === "DRAFT" ||
    value === "SUBMITTED" ||
    value === "COMPLETED" ||
    value === "REVIEWED"
  ) {
    return value;
  }

  return "DRAFT";
}

function extractMutationSummary(
  data: ApiMeetingSummaryMutationResponse | ApiSummaryDetail,
): ApiSummaryDetail {
  if ("meetingSummary" in data && data.meetingSummary) {
    return data.meetingSummary;
  }

  return data as ApiSummaryDetail;
}

function extractSummaryId(
  data: ApiMeetingSummaryMutationResponse | ApiSummaryDetail,
): number {
  const summary = extractMutationSummary(data);

  if (!Number.isFinite(summary.id) || summary.id <= 0) {
    throw new Error("Meeting summary ID was not returned by the server.");
  }

  return summary.id;
}

function getEditableIssueText(value?: string) {
  if (!value || value === "Not Uploaded") return undefined;
  const trimmed = value.trim();
  return trimmed || undefined;
}

// Only resolution statuses are valid in issueResolves. Issue workflow labels
// like "SUBMITTED" must be omitted or the API returns 400.
function normalizeIssueResolveStatusForPayload(
  status?: string,
): string | undefined {
  if (!status?.trim()) return undefined;

  const trimmed = status.trim();
  const normalized = trimmed.toUpperCase().replace(/\s+/g, "_");

  if (normalized === "NEW_SUBMISSION" || normalized === "NEW_SUBMITTED") {
    return undefined;
  }

  if (normalized === "NOT_ADDRESS" || normalized === "NOT_ADDRESSED") {
    return "Not Addressed";
  }

  if (normalized === "IN_PROGRESS") {
    return "In Progress";
  }

  if (normalized === "SOLVED") {
    return "Solved";
  }

  if (
    trimmed === "In Progress" ||
    trimmed === "Solved" ||
    trimmed === "Not Addressed" ||
    trimmed === "Not Address"
  ) {
    return trimmed === "Not Address" ? "Not Addressed" : trimmed;
  }

  return undefined;
}

type IssueEscalationPayloadValue = "CDC" | "CEFP" | null | undefined;

function normalizeIssueEscalationForPayload(
  escalation?: string,
  options: { clearWhenEmpty?: boolean } = {},
): IssueEscalationPayloadValue {
  const trimmed = escalation?.trim();
  if (!trimmed || trimmed === "Not Uploaded") {
    return options.clearWhenEmpty ? null : undefined;
  }
  if (trimmed === "CDC" || trimmed === "CEFP") return trimmed;
  return options.clearWhenEmpty ? null : undefined;
}

export function buildAgencyStakeholderIdsForIssue(
  issue: Pick<
    ViewMeetingSummaryIssueRow,
    "secondAgency" | "thirdAgency" | "fourthAgency" | "fifthAgency" | "agencies"
  >,
): number[] | undefined {
  const slotNames = [
    issue.secondAgency,
    issue.thirdAgency,
    issue.fourthAgency,
    issue.fifthAgency,
  ];
  const ids: number[] = [];

  for (const name of slotNames) {
    if (!name || name === "Not Uploaded" || name === "-") {
      continue;
    }

    const matched = issue.agencies?.find(
      (agency) =>
        agency.name?.trim().toLowerCase() === name.trim().toLowerCase(),
    );

    if (matched?.id) {
      ids.push(matched.id);
    }
  }

  return ids.length > 0 ? ids : undefined;
}

function buildIssueResolvePayload(
  input: UpsertMeetingSummaryIssueInput,
  options: { clearWhenEmpty: boolean },
) {
  const payload: Record<string, string | number[] | null> = {};

  const status = normalizeIssueResolveStatusForPayload(input.status);
  if (status) {
    payload.status = status;
  }

  const escalate = normalizeIssueEscalationForPayload(
    input.issueEscalation,
    options,
  );
  if (escalate !== undefined) {
    payload.escalate = escalate;
  }

  const rgcDecision = getEditableIssueText(input.rgcDecision);
  if (rgcDecision) payload.rgcDecision = rgcDecision;

  const nextStep = getEditableIssueText(input.nextStep);
  if (nextStep) payload.nextStep = nextStep;

  const remark = getEditableIssueText(input.remark);
  if (remark) payload.remark = remark;

  const agencyStakeholderIds =
    input.agencyStakeholderIds && input.agencyStakeholderIds.length > 0
      ? input.agencyStakeholderIds
      : undefined;
  if (agencyStakeholderIds) {
    payload.agencyStakeholderIds = agencyStakeholderIds;
  }

  return payload;
}

function appendIssueResolvePayloadToFormData(
  formData: FormData,
  payload: Record<string, string | number[] | null>,
) {
  for (const [key, value] of Object.entries(payload)) {
    if (key === "agencyStakeholderIds" && Array.isArray(value)) {
      formData.append(key, value.join(","));
      continue;
    }

    formData.append(key, String(value));
  }
}

function appendParticipantsToFormData(
  formData: FormData,
  participants?: MeetingSummaryParticipants,
) {
  if (!participants) return;

  for (const [key, value] of Object.entries(participants)) {
    formData.append(key, value);
  }
}

function buildIssueResolveJsonBody(
  payload: Record<string, string | number[] | null>,
) {
  const { agencyStakeholderIds, ...rest } = payload;

  return {
    ...rest,
    ...(Array.isArray(agencyStakeholderIds) && agencyStakeholderIds.length > 0
      ? { agencyStakeholderIds }
      : {}),
  };
}

export function buildIssueResolvesFromRows(
  issues: ViewMeetingSummaryIssueRow[],
): MeetingSummaryIssueResolvePayload[] {
  return issues.flatMap((issue) => {
    const agencyStakeholderIds = buildAgencyStakeholderIdsForIssue(issue);
    const fields = buildIssueResolvePayload(
      {
        status: issue.status,
        issueEscalation: issue.issueEscalation,
        rgcDecision: issue.rgcDecision,
        nextStep: issue.nextStep,
        remark: issue.remark,
        agencyStakeholderIds,
      },
      { clearWhenEmpty: false },
    );

    if (Object.keys(fields).length === 0) {
      return [];
    }

    return [
      {
        issueId: issue.id,
        ...(fields as Omit<MeetingSummaryIssueResolvePayload, "issueId">),
      },
    ];
  });
}

function appendIssueResolvesToFormData(
  formData: FormData,
  issueResolves?: MeetingSummaryIssueResolvePayload[],
) {
  if (!issueResolves?.length) return;

  formData.append("issueResolves", JSON.stringify(issueResolves));
}

function stripHtml(value?: string | null) {
  if (!value) return "";
  return value
    .replace(/<[^>]*>/g, " ")
    .replace(/&amp;/g, "&")
    .replace(/&nbsp;/g, " ")
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/\s+/g, " ")
    .trim();
}

// Rich-text editor values are stored as JSON; render them as readable text.
function toPlainText(value: unknown): string {
  if (value == null) return "";
  if (typeof value === "string") return stripHtml(value);
  try {
    return stripHtml(JSON.stringify(value));
  } catch {
    return "";
  }
}

function formatTimeRange(start?: string | null, end?: string | null) {
  const format = (value?: string | null) => {
    if (!value) return "";
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return "";
    return new Intl.DateTimeFormat("en-US", {
      hour: "numeric",
      minute: "2-digit",
      hour12: true,
      timeZone: "UTC",
    }).format(date);
  };

  const startLabel = format(start);
  const endLabel = format(end);
  if (startLabel && endLabel) return `${startLabel} - ${endLabel}`;
  return startLabel || endLabel || "-";
}

// The resolve status tokens map to the labels the issue table shows.
function mapIssueStatusLabel(status?: string | null): string {
  switch (status) {
    case "SOLVED":
      return "Solved";
    case "IN_PROGRESS":
      return "In Progress";
    case "NOT_ADDRESSED":
      return "Not Addressed";
    case "NEW_SUBMISSION":
      return "New Submitted";
    default:
      return "New Submitted";
  }
}

function mapApiIssueToViewRow(
  issue: ApiSummaryIssue,
  wgName: string,
): ViewMeetingSummaryIssueRow {
  const agencyFields = [
    {
      name: issue.secondAgency,
      logo: issue.secondAgencyLogo,
      fallbackIndex: 0,
    },
    {
      name: issue.thirdAgency,
      logo: issue.thirdAgencyLogo,
      fallbackIndex: 1,
    },
    {
      name: issue.fourthAgency,
      logo: issue.fourthAgencyLogo,
      fallbackIndex: 2,
    },
    {
      name: issue.fifthAgency,
      logo: issue.fifthAgencyLogo,
      fallbackIndex: 3,
    },
  ] as const;

  const getAgencyName = (field: (typeof agencyFields)[number]) => {
    const direct = field.name?.trim();
    if (direct && direct !== "Not Uploaded" && direct !== "-") {
      return direct;
    }

    return issue.agencies?.[field.fallbackIndex]?.name?.trim() || "Not Uploaded";
  };

  const getAgencyLogo = (
    field: (typeof agencyFields)[number],
    agencyName: string,
  ) => {
    const direct = field.logo?.trim();
    const fromApi = direct
      ? getMediaUrl(direct)
      : getMediaUrl(issue.agencies?.[field.fallbackIndex]?.logo);

    return resolveAgencyLogoForDisplay(agencyName, fromApi);
  };

  return {
    id: issue.id,
    wgName,
    issue: stripHtml(issue.issue) || "-",
    category: issue.category?.trim() || "-",
    issueDescription: stripHtml(issue.issueDescription) || "-",
    recommendation: stripHtml(issue.recommendation) || "-",
    rgcDecision: toPlainText(issue.rgcDecision) || "Not Uploaded",
    nextStep: toPlainText(issue.nextStep) || "Not Uploaded",
    remark: toPlainText(issue.remark) || "Not Uploaded",
    issueReference: issue.referenceDocument?.trim() || "",
    status: mapIssueStatusLabel(issue.status),
    primaryAgency: issue.primaryAgency?.trim() || "Not Uploaded",
    primaryAgencyLogo: resolveAgencyLogoForDisplay(
      issue.primaryAgency?.trim() || "Not Uploaded",
      getMediaUrl(issue.primaryAgencyLogo),
    ),
    secondAgency: getAgencyName(agencyFields[0]),
    secondAgencyLogo: getAgencyLogo(
      agencyFields[0],
      getAgencyName(agencyFields[0]),
    ),
    thirdAgency: getAgencyName(agencyFields[1]),
    thirdAgencyLogo: getAgencyLogo(
      agencyFields[1],
      getAgencyName(agencyFields[1]),
    ),
    fourthAgency: getAgencyName(agencyFields[2]),
    fourthAgencyLogo: getAgencyLogo(
      agencyFields[2],
      getAgencyName(agencyFields[2]),
    ),
    fifthAgency: getAgencyName(agencyFields[3]),
    fifthAgencyLogo: getAgencyLogo(
      agencyFields[3],
      getAgencyName(agencyFields[3]),
    ),
    issueEscalation: issue.escalation?.trim() || "Not Uploaded",
    agencies: (issue.agencies ?? [])
      .filter((agency) => agency.id > 0 && agency.name?.trim())
      .map((agency) => ({
        id: agency.id,
        name: agency.name?.trim() || "Not Uploaded",
        logo: getMediaUrl(agency.logo),
      })),
  };
}

export async function upsertMeetingSummaryIssue(
  summaryId: number,
  issueId: number,
  input: UpsertMeetingSummaryIssueInput,
  wgName: string,
): Promise<ViewMeetingSummaryIssueRow> {
  const payload = buildIssueResolvePayload(input, { clearWhenEmpty: true });
  const path = `/meeting-summaries/${summaryId}/issues/${issueId}`;

  const data = input.attachment
    ? await apiFetch<ApiMeetingSummaryMutationResponse>(path, {
        method: "PATCH",
        body: (() => {
          const formData = new FormData();
          appendIssueResolvePayloadToFormData(formData, payload);
          formData.append("documentReference", input.attachment as File);
          return formData;
        })(),
      })
    : await apiFetch<ApiMeetingSummaryMutationResponse>(path, {
        method: "PATCH",
        body: JSON.stringify(buildIssueResolveJsonBody(payload)),
      });

  const summary = extractMutationSummary(data);
  const savedIssue = summary.issues?.find(
    (issue) => Number(issue.id) === Number(issueId),
  );

  if (!savedIssue) {
    throw new Error("Issue resolution was saved but the issue was not returned.");
  }

  return mapApiIssueToViewRow(savedIssue, wgName);
}

function mapApiSummaryDetailToView(
  detail: ApiSummaryDetail,
): ViewMeetingSummaryDetail {
  const request = detail.meetingRequest;
  const meeting = detail.meeting;
  const wgName = request?.pswg?.trim() || "-";
  const meetingRequestPath = request?.meetingRequestLetter?.trim() || "";
  const meetingDocumentPath =
    meeting?.documentReference?.trim() ||
    request?.meetingRequestLetter?.trim() ||
    "";
  const summaryDocumentPath = detail.documentReference?.trim() || "";

  return {
    id: detail.id,
    status: normalizeSummaryStatus(detail.status),
    share: Boolean(detail.share),
    privateSectorWg: wgName,
    meetingRequestDocument: meetingRequestPath,
    meetingRequestDocumentSize: "",
    wgMeeting: wgName,
    meetingDocument: meetingDocumentPath,
    meetingDocumentSize: "",
    governmentAgency: request?.governmentAgency?.trim() || "-",
    governmentAgencyLogo: getMediaUrl(request?.governmentAgencyLogo),
    sentBy: request?.submittedBy?.trim() || "-",
    time: formatTimeRange(meeting?.startTime, meeting?.endTime),
    date: formatDate(meeting?.meetingDate),
    location: meeting?.location?.trim() || "-",
    participants: {
      pswgReporter: detail.pswgReporter?.trim() || "",
      pswgReporterPosition: detail.pswgReporterPosition?.trim() || "",
      pswgRepresentative: detail.pswgRepresentative?.trim() || "",
      pswgRepresentativePosition:
        detail.pswgRepresentativePosition?.trim() || "",
      ministryReporter: detail.ministryReporter?.trim() || "",
      ministryReporterPosition: detail.ministryReporterPosition?.trim() || "",
      ministryRepresentative: detail.ministryRepresentative?.trim() || "",
      ministryRepresentativePosition:
        detail.ministryRepresentativePosition?.trim() || "",
    },
    summaryReference: {
      name: summaryDocumentPath,
      size: "",
      uploadStatus: summaryDocumentPath ? "Completed" : "Uploading",
      url: getMediaUrl(detail.documentReference),
    },
    issues: (detail.issues ?? []).map((issue) =>
      mapApiIssueToViewRow(issue, wgName),
    ),
  };
}

export async function getMeetingSummaryById(
  id: number,
): Promise<ViewMeetingSummaryDetail> {
  const detail = await apiFetch<ApiSummaryDetail>(`/meeting-summaries/${id}`);

  return mapApiSummaryDetailToView(detail);
}
