import { redirectToLoginAfterSessionExpired } from "@/features/auth/service/auth-service";
import type {
  IssueRow,
  IssueStatus,
} from "@/features/pswg/working-group-issues/wg-issues-data";
import type { WgIssuesLanguage } from "@/features/pswg/working-group-issues/wg-issues-i18n";
import type { AttachmentFilterValue } from "@/features/pswg/working-group-issues/components/wg-issues-filters";

export type WorkingGroupIssueLookup = {
  id: number;
  // Stable identifier (e.g. "DRAFT", "NEW_SUBMISSION"). Names can be edited
  // in admin UI; codes don't change, so default-status detection uses code.
  code?: string;
  name: string;
};

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

export type WorkingGroupIssuePrimaryGovernment = {
  governmentAgency: WorkingGroupIssueAgency;
  workingGroup: {
    id: number;
    name: string;
  };
};

export type CreateWorkingGroupIssueAgency = {
  agencyOrder: number;
  stakeholderId: number;
};

export type CreateWorkingGroupIssuePayload = {
  attachmentFile?: File | null;
  description: string;
  governmentAgencies: CreateWorkingGroupIssueAgency[];
  // Optional — when omitted the backend assigns the default category.
  categoryId?: number | null;
  issueStatusId: number;
  recommendation: string;
  title: string;
};

export type UpdateWorkingGroupIssuePayload = CreateWorkingGroupIssuePayload;

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

type PaginationMeta = {
  page: number;
  limit: number;
  total: number;
  totalPages: number;
};

type PaginatedData<T> = {
  items: T[];
  meta: PaginationMeta;
};

type ApiWorkingGroupIssue = {
  attachment?: string | null;
  createdAt?: string | null;
  description?: string | null;
  governmentAgencies?: {
    agencyOrder: number;
    stakeholder?: {
      id: number;
      name?: string | null;
      logo?: string | null;
    } | null;
  }[];
  id: number;
  category?: {
    id: number;
    name?: string | null;
  } | null;
  issueStatus?: {
    id: number;
    code?: string | null;
    name?: string | null;
  } | null;
  meetingRequest?: {
    id: number;
    title?: string | null;
    // Scheduled meeting(s) for this request, newest first. Used for the
    // issue's "Meeting Date".
    meetings?: {
      id: number;
      meetingDate?: string | null;
      startTime?: string | null;
      endTime?: string | null;
    }[] | null;
  } | null;
  recommendation?: string | null;
  stakeholder?: {
    id: number;
    name?: string | null;
    logo?: string | null;
  } | null;
  title?: string | null;
};

const API_URL =
  process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api/v1";
const BACKEND_ASSET_URL = API_URL.replace(/\/api(?:\/v\d+)?\/?$/, "");

function resolveAssetUrl(value: string | null | undefined) {
  if (!value) {
    return null;
  }

  if (
    value.startsWith("http://") ||
    value.startsWith("https://") ||
    value.startsWith("data:") ||
    value.startsWith("blob:")
  ) {
    return value;
  }

  if (value.startsWith("/uploads/")) {
    return `${BACKEND_ASSET_URL}${value}`;
  }

  return value;
}

function normalizeWorkingGroupIssueAgency(
  agency: WorkingGroupIssueAgency,
): WorkingGroupIssueAgency {
  return {
    ...agency,
    logo: resolveAssetUrl(agency.logo),
  };
}

function getApiErrorMessage(body: unknown, status: number) {
  if (body && typeof body === "object" && "message" in body) {
    const message = (body as { message?: unknown }).message;

    if (Array.isArray(message)) {
      return message.join(" ");
    }

    if (typeof message === "string" && message.trim()) {
      return message;
    }
  }

  if (status === 400) return "Please check the issue form.";
  if (status === 401) return "Unauthorized";
  if (status === 403) return "You do not have permission for this action.";
  if (status === 404) return "Selected form data was not found.";

  return `Request failed (${status}).`;
}

async function parseResponseBody(response: Response) {
  const text = await response.text();

  if (!text) {
    return null;
  }

  try {
    return JSON.parse(text) as unknown;
  } catch {
    return text;
  }
}

async function jsonRequest<T>(path: string): Promise<T> {
  const response = await fetch(`${API_URL}${path}`, {
    method: "GET",
    credentials: "include",
    headers: {
      "Content-Type": "application/json",
    },
  });

  const responseBody = await parseResponseBody(response);

  if (!response.ok) {
    if (response.status === 401) {
      redirectToLoginAfterSessionExpired();
    }

    throw new Error(getApiErrorMessage(responseBody, response.status));
  }

  return responseBody as T;
}

async function formRequest<T>(
  path: string,
  method: "POST" | "PATCH",
  formData: FormData,
): Promise<T> {
  const response = await fetch(`${API_URL}${path}`, {
    method,
    credentials: "include",
    body: formData,
  });

  const responseBody = await parseResponseBody(response);

  if (!response.ok) {
    if (response.status === 401) {
      redirectToLoginAfterSessionExpired();
    }

    throw new Error(getApiErrorMessage(responseBody, response.status));
  }

  return responseBody as T;
}

async function deleteRequest<T>(path: string): Promise<T> {
  const response = await fetch(`${API_URL}${path}`, {
    method: "DELETE",
    credentials: "include",
    headers: {
      "Content-Type": "application/json",
    },
  });

  const responseBody = await parseResponseBody(response);

  if (!response.ok) {
    if (response.status === 401) {
      redirectToLoginAfterSessionExpired();
    }

    throw new Error(getApiErrorMessage(responseBody, response.status));
  }

  return responseBody as T;
}

function appendText(formData: FormData, key: string, value: string | number) {
  formData.append(key, String(value));
}

function buildIssueFormData(payload: CreateWorkingGroupIssuePayload) {
  const formData = new FormData();

  appendText(formData, "title", payload.title);
  appendText(formData, "description", payload.description);
  appendText(formData, "recommendation", payload.recommendation);
  appendText(formData, "issueStatusId", payload.issueStatusId);
  appendText(
    formData,
    "governmentAgencies",
    JSON.stringify(payload.governmentAgencies),
  );

  if (
    payload.categoryId !== undefined &&
    payload.categoryId !== null
  ) {
    appendText(formData, "categoryId", payload.categoryId);
  }

  if (payload.attachmentFile) {
    formData.append("attachmentFile", payload.attachmentFile);
  }

  return formData;
}

const ISSUE_STATUSES: IssueStatus[] = [
  "Solved",
  "In Progress",
  "Not Addressed",
  "Draft",
  "Saved",
  "New Submission",
];

// Stable backend codes mapped to the UI label union.
const ISSUE_STATUS_BY_CODE: Record<string, IssueStatus> = {
  DRAFT: "Draft",
  SAVED: "Saved",
  NEW_SUBMISSION: "New Submission",
  IN_PROGRESS: "In Progress",
  SOLVED: "Solved",
  NOT_ADDRESSED: "Not Addressed",
};

// Resolve the backend's status to the strict UI union. Prefer the stable
// `code` ("DRAFT", "NEW_SUBMISSION", ...) since names can be edited; fall
// back to a case-insensitive name match for older API responses. Unknown
// or missing values default to "Draft" so an un-classified issue stays in
// the safest bucket.
function normalizeIssueStatus(
  status?: { code?: string | null; name?: string | null } | null,
): IssueStatus {
  if (status?.code) {
    const fromCode = ISSUE_STATUS_BY_CODE[status.code.trim().toUpperCase()];
    if (fromCode) return fromCode;
  }

  if (status?.name) {
    const normalized = status.name.trim().toLowerCase();
    const fromName = ISSUE_STATUSES.find(
      (item) => item.toLowerCase() === normalized,
    );
    if (fromName) return fromName;
  }

  return "Draft";
}

function getIssueYear(createdAt: string | null | undefined) {
  if (!createdAt) {
    return null;
  }

  const createdDate = new Date(createdAt);

  if (Number.isNaN(createdDate.getTime())) {
    return null;
  }

  return String(createdDate.getFullYear());
}

// Format an issue's submitted date (its creation date) for the list/grid view.
function formatSubmittedDate(value: string | null | undefined): string | null {
  if (!value) {
    return null;
  }

  const date = new Date(value);

  if (Number.isNaN(date.getTime())) {
    return null;
  }

  return date.toLocaleDateString("en-US", {
    day: "numeric",
    month: "long",
    year: "numeric",
  });
}

function mapWorkingGroupIssue(
  issue: ApiWorkingGroupIssue,
  index: number,
): IssueRow {
  const primaryAgency = (issue.governmentAgencies ?? []).sort(
    (firstAgency, secondAgency) =>
      firstAgency.agencyOrder - secondAgency.agencyOrder,
  )[0];

  return {
    id: issue.id,
    issue: issue.title ?? "-",
    category: issue.category?.name ?? "-",
    description: issue.description ?? "-",
    recommendation: issue.recommendation ?? "-",
    governmentDecision: "-",
    hasAttachment: Boolean(issue.attachment?.trim()),
    status: normalizeIssueStatus(issue.issueStatus),
    primaryAgency: primaryAgency?.stakeholder?.name ?? "-",
    primaryAgencyLogo: resolveAssetUrl(primaryAgency?.stakeholder?.logo),
    issueReference: `Issue ${index + 1}`,
    solutionReference: undefined,
    submittedDate: formatSubmittedDate(issue.createdAt),
    year: getIssueYear(issue.createdAt),
  };
}

export async function getWorkingGroupIssues() {
  const response = await jsonRequest<
    ApiResponse<PaginatedData<ApiWorkingGroupIssue>>
  >("/working-group-issues/my");

  const items = response.data?.items;

  return Array.isArray(items) ? items.map(mapWorkingGroupIssue) : [];
}

// The fuller shape the View Detail page needs (more than the table row has).
export type WorkingGroupIssueDetail = {
  attachment: string | null;
  category: string;
  description: string;
  governmentAgencies: CreateWorkingGroupIssueAgency[];
  id: number;
  categoryId: number | null;
  issueStatusId: number | null;
  meetingDate: string | null;
  meetingRequestTitle: string | null;
  primaryAgencyName: string;
  primaryAgencyLogo: string | null;
  recommendation: string;
  status: IssueStatus;
  submittedDate: string | null;
  title: string;
  workingGroupName: string;
};

type ApiMeeting = {
  id: number;
  meetingDate?: string | null;
  startTime?: string | null;
  endTime?: string | null;
};

// Format a single time value (stored as a time-only field, so read in UTC to
// avoid shifting the hour by the local timezone).
function formatMeetingTime(value?: string | null): string {
  if (!value) {
    return "";
  }

  const time = new Date(value);

  if (Number.isNaN(time.getTime())) {
    return "";
  }

  return new Intl.DateTimeFormat("en-US", {
    hour: "numeric",
    minute: "2-digit",
    hour12: true,
    timeZone: "UTC",
  }).format(time);
}

// Format the issue's meeting date and time for display. Picks the newest
// scheduled meeting that has a date (the backend returns them newest-first).
function getMeetingDate(
  meetings: ApiMeeting[] | null | undefined,
): string | null {
  const meeting = (meetings ?? []).find((item) => item?.meetingDate);

  if (!meeting?.meetingDate) {
    return null;
  }

  const date = new Date(meeting.meetingDate);

  if (Number.isNaN(date.getTime())) {
    return null;
  }

  const dateLabel = date.toLocaleDateString("en-US", {
    day: "numeric",
    month: "long",
    year: "numeric",
    timeZone: "UTC",
  });

  const startLabel = formatMeetingTime(meeting.startTime);
  const endLabel = formatMeetingTime(meeting.endTime);

  // Combine the date with the time range when times are available.
  const timeLabel =
    startLabel && endLabel
      ? `${startLabel} - ${endLabel}`
      : startLabel || endLabel;

  return timeLabel ? `${dateLabel} · ${timeLabel}` : dateLabel;
}

// Map the full issue (from GET /:id) into the shape the detail page needs.
function mapWorkingGroupIssueDetail(
  issue: ApiWorkingGroupIssue,
): WorkingGroupIssueDetail {
  const primaryAgency = (issue.governmentAgencies ?? []).sort(
    (firstAgency, secondAgency) =>
      firstAgency.agencyOrder - secondAgency.agencyOrder,
  )[0];

  return {
    attachment: resolveAssetUrl(issue.attachment),
    category: issue.category?.name ?? "-",
    description: issue.description ?? "-",
    governmentAgencies: (issue.governmentAgencies ?? [])
      .map((agency) => ({
        agencyOrder: agency.agencyOrder,
        stakeholderId: agency.stakeholder?.id ?? 0,
      }))
      .filter((agency) => agency.stakeholderId > 0)
      .sort(
        (firstAgency, secondAgency) =>
          firstAgency.agencyOrder - secondAgency.agencyOrder,
      ),
    id: issue.id,
    categoryId: issue.category?.id ?? null,
    issueStatusId: issue.issueStatus?.id ?? null,
    meetingDate: getMeetingDate(issue.meetingRequest?.meetings),
    meetingRequestTitle: issue.meetingRequest?.title ?? null,
    primaryAgencyName: primaryAgency?.stakeholder?.name ?? "-",
    primaryAgencyLogo: resolveAssetUrl(primaryAgency?.stakeholder?.logo),
    recommendation: issue.recommendation ?? "-",
    status: normalizeIssueStatus(issue.issueStatus),
    submittedDate: issue.createdAt ?? null,
    title: issue.title ?? "-",
    workingGroupName: issue.stakeholder?.name ?? "",
  };
}

export async function getWorkingGroupIssueById(id: number) {
  const response = await jsonRequest<ApiResponse<ApiWorkingGroupIssue>>(
    `/working-group-issues/${id}`,
  );

  return mapWorkingGroupIssueDetail(response.data);
}

export type WorkingGroupIssueSummary = {
  totalIssues: number;
  solved: number;
  inProgress: number;
  notAddressed: number;
};

export async function getWorkingGroupIssueSummary() {
  const response = await jsonRequest<ApiResponse<WorkingGroupIssueSummary>>(
    "/working-group-issues/summary/my",
  );

  return response.data;
}

export async function getWorkingGroupIssueStatuses() {
  const response = await jsonRequest<ApiResponse<WorkingGroupIssueLookup[]>>(
    "/working-group-issues/statuses",
  );

  return Array.isArray(response.data) ? response.data : [];
}

export async function getWorkingGroupCategories() {
  const response = await jsonRequest<ApiResponse<WorkingGroupIssueLookup[]>>(
    "/working-group-issues/categories",
  );

  return Array.isArray(response.data) ? response.data : [];
}

export async function getWorkingGroupIssueGovernmentAgencies() {
  const response = await jsonRequest<ApiResponse<WorkingGroupIssueAgency[]>>(
    "/working-group-issues/government-agencies",
  );

  return Array.isArray(response.data)
    ? response.data.map(normalizeWorkingGroupIssueAgency)
    : [];
}

export async function getMyWorkingGroupIssuePrimaryGovernment() {
  const response = await jsonRequest<
    ApiResponse<WorkingGroupIssuePrimaryGovernment>
  >("/working-group-issues/my/primary-goverment");

  return {
    ...response.data,
    governmentAgency: normalizeWorkingGroupIssueAgency(
      response.data.governmentAgency,
    ),
  };
}

export async function createWorkingGroupIssue(
  payload: CreateWorkingGroupIssuePayload,
) {
  const formData = buildIssueFormData(payload);

  const response = await formRequest<ApiResponse<unknown>>(
    "/working-group-issues",
    "POST",
    formData,
  );

  return response.message;
}

export async function updateWorkingGroupIssue(
  id: number,
  payload: UpdateWorkingGroupIssuePayload,
) {
  const formData = buildIssueFormData(payload);

  const response = await formRequest<ApiResponse<unknown>>(
    `/working-group-issues/${id}`,
    "PATCH",
    formData,
  );

  return response.message;
}

export async function deleteWorkingGroupIssue(id: number) {
  const response = await deleteRequest<ApiResponse<unknown>>(
    `/working-group-issues/${id}`,
  );

  return response.message;
}

export type ExportWorkingGroupIssuesParams = {
  language: WgIssuesLanguage;
  selectedStatuses: IssueStatus[];
  selectedCategories: string[];
  selectedPrimaryAgencies: string[];
  selectedYears: string[];
  selectedAttachments: AttachmentFilterValue[];
  statuses: WorkingGroupIssueLookup[];
  categories: WorkingGroupIssueLookup[];
  agencies: WorkingGroupIssueAgency[];
};

function findLookupIdsByNames(
  list: Array<{ id: number; name: string }>,
  names: string[],
): number[] {
  return names
    .map((name) => {
      const item = list.find(
        (entry) => entry.name.toLowerCase() === name.toLowerCase(),
      );
      return item?.id;
    })
    .filter((id): id is number => id !== undefined);
}

function buildExportQuery(params: ExportWorkingGroupIssuesParams): string {
  const searchParams = new URLSearchParams();
  searchParams.set("lang", params.language);

  const issueStatusIds = findLookupIdsByNames(
    params.statuses,
    params.selectedStatuses,
  );
  issueStatusIds.forEach((id) => {
    searchParams.append("issueStatusIds", String(id));
  });

  const categoryIds = findLookupIdsByNames(
    params.categories,
    params.selectedCategories,
  );
  categoryIds.forEach((id) => {
    searchParams.append("categoryIds", String(id));
  });

  const primaryAgencyIds = findLookupIdsByNames(
    params.agencies,
    params.selectedPrimaryAgencies,
  );
  primaryAgencyIds.forEach((id) => {
    searchParams.append("primaryAgencyIds", String(id));
  });

  params.selectedYears.forEach((year) => {
    const parsedYear = Number(year);
    if (Number.isInteger(parsedYear)) {
      searchParams.append("years", String(parsedYear));
    }
  });

  if (params.selectedAttachments.length === 1) {
    searchParams.set(
      "hasAttachment",
      params.selectedAttachments[0] === "yes" ? "true" : "false",
    );
  }

  const query = searchParams.toString();
  return query ? `?${query}` : "";
}

function getExportFilename(contentDisposition: string | null): string {
  if (!contentDisposition) {
    return "working-group-issues.xlsx";
  }

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

function triggerBrowserDownload(blob: Blob, filename: string) {
  const objectUrl = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = objectUrl;
  anchor.download = filename;
  anchor.click();
  URL.revokeObjectURL(objectUrl);
}

export async function exportWorkingGroupIssues(
  params: ExportWorkingGroupIssuesParams,
) {
  const query = buildExportQuery(params);
  const response = await fetch(
    `${API_URL}/working-group-issues/my/export${query}`,
    {
      method: "GET",
      credentials: "include",
    },
  );

  if (!response.ok) {
    if (response.status === 401) {
      redirectToLoginAfterSessionExpired();
    }

    const responseBody = await parseResponseBody(response);
    throw new Error(getApiErrorMessage(responseBody, response.status));
  }

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

  triggerBrowserDownload(blob, filename);
}
