const API_BASE_URL = (
  process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api/v1"
).replace(/\/$/, "");

const API_ASSET_ORIGIN = (() => {
  try {
    return new URL(API_BASE_URL).origin;
  } catch {
    return API_BASE_URL.replace(/\/api\/v\d+(?:\/.*)?$/, "");
  }
})();

export function resolveAssetUrl(path?: string | null): string | null {
  if (!path) return null;

  const cleanPath = path.trim();
  if (!cleanPath) return null;

  if (/^(https?:)?\/\//i.test(cleanPath) || cleanPath.startsWith("data:")) {
    return cleanPath;
  }

  if (cleanPath.startsWith("/")) {
    return `${API_ASSET_ORIGIN}${cleanPath}`;
  }

  return `${API_ASSET_ORIGIN}/${cleanPath}`;
}

function normalizeGovernmentAgency(
  agency: ApiGovernmentAgency,
): ApiGovernmentAgency {
  return {
    ...agency,
    logo: resolveAssetUrl(agency.logo),
  };
}

function normalizeStakeholder(stakeholder: ApiStakeholder): ApiStakeholder {
  return {
    ...stakeholder,
    logo: resolveAssetUrl(stakeholder.logo),
  };
}

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

export type MeetingRequestStatus =
  | "Drafted"
  | "Submitted"
  | "Under Review"
  | "Scheduled"
  | "Completed";

function normalizeMeetingRequestStatus(
  status?: string | null,
): MeetingRequestStatus {
  if (status === "DRAFT" || status === "Draft" || status === "Drafted") {
    return "Drafted";
  }

  if (status === "SUBMITTED" || status === "Submitted") {
    return "Submitted";
  }

  if (status === "UNDER_REVIEW" || status === "Under Review") {
    return "Under Review";
  }

  if (status === "SCHEDULED" || status === "Scheduled") {
    return "Scheduled";
  }

  if (status === "COMPLETED" || status === "Completed") {
    return "Completed";
  }

  return "Submitted";
}

export type ApiStakeholderType = {
  id: number;
  name: string;
};

export type ApiGovernmentAgency = {
  id: number;
  name: string;
  logo?: string | null;
  stakeholderTypeId?: number;
  stakeholderType?: ApiStakeholderType | null;
  relatedStakeholderId?: number | null;
  active?: boolean;
};

export type ApiPrimaryGovernment = {
  workingGroup: {
    id: number;
    name: string;
  };
  governmentAgency: ApiGovernmentAgency;
};

export type ApiCategory = {
  id: number;
  name: string;
};

export type ApiIssueStatus = {
  id: number;
  name: string;
  code?: string | null;
};

export type ApiUser = {
  id: number;
  email: string;
  name: string;
  avatar?: string | null;
  position?: string | null;
  isActive?: boolean;
  createdAt?: string;
  updatedAt?: string;
  deletedAt?: string | null;
};

export type ApiStakeholder = {
  id: number;
  logo?: string | null;
  name: string;
  description?: string | null;
  stakeholderTypeId?: number;
  stakeholderType?: ApiStakeholderType | null;
  relatedStakeholderId?: number | null;
  active?: boolean;
  createdAt?: string;
  updatedAt?: string;
  deletedAt?: string | null;
};

export type ApiWorkingGroup = ApiStakeholder;

export type ApiGovernmentAgencyMeetingRequest = {
  stakeholderId: number;
  meetingRequestId: number;
  stakeholder: ApiStakeholder;
};

export type ApiIssue = {
  id: number;
  title: string;
  description?: string | null;
  recommendation?: string | null;
  attachment?: string | null;
  issueStatusId?: number;
  categoryId?: number;
  meetingRequestId?: number | null;
  userId?: number;
  issueStatus?: ApiIssueStatus;
  category?: ApiCategory;
  createdAt?: string;
  updatedAt?: string;
  deletedAt?: string | null;
};

export type ApiMeetingRequest = {
  id: number;
  title: string;
  description: string;
  meetingRequestLetter: string;
  status: MeetingRequestStatus;
  userId: number;
  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
  canManage?: boolean;
  user?: ApiUser;
  governmentAgencies?: ApiGovernmentAgencyMeetingRequest[];
  issues?: ApiIssue[];
};

function normalizeMeetingRequest(
  meetingRequest: ApiMeetingRequest,
): ApiMeetingRequest {
  return {
    ...meetingRequest,
    status: normalizeMeetingRequestStatus(meetingRequest.status),
  };
}

type SendMeetingRequestResponse = ApiMeetingRequest | {
  data?: ApiMeetingRequest | {
    data?: ApiMeetingRequest;
  };
};

export type CreateMeetingRequestPayload = {
  title: string;
  description: string;
  meetingRequestLetter: File | string;
  userId?: number;
  stakeholderIds: number[];
  issueIds?: number[];
};

export type UpdateMeetingRequestPayload = Partial<CreateMeetingRequestPayload>;

export type CreateWorkingGroupIssuePayload = {
  title: string;
  description: string;
  recommendation: string;
  categoryId: number;
  issueStatusId: number;
  stakeholderIds: number[];
  attachmentFile?: File | null;
};

type CreateWorkingGroupIssueResponse =
  | ApiIssue
  | {
      issue?: ApiIssue;
    };

function findTokenFromObject(value: unknown): string | null {
  if (!value || typeof value !== "object") return null;

  const record = value as Record<string, unknown>;
  const tokenKeys = ["accessToken", "access_token", "token", "authToken", "jwt"];

  for (const key of tokenKeys) {
    const token = record[key];

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

  for (const childValue of Object.values(record)) {
    const token = findTokenFromObject(childValue);
    if (token) return token;
  }

  return null;
}

function getToken(): string | null {
  if (typeof window === "undefined") return null;

  const directKeys = [
    "accessToken",
    "access_token",
    "token",
    "authToken",
    "jwt",
    "AUTH_TOKEN",
  ];

  for (const key of directKeys) {
    const token = localStorage.getItem(key);

    if (token && token.trim()) {
      return token;
    }
  }

  const objectKeys = [
    "auth",
    "user",
    "auth-storage",
    "authStore",
    "gpsf-auth-storage",
    "persist:auth",
    "persist:root",
  ];

  for (const key of objectKeys) {
    const rawValue = localStorage.getItem(key);

    if (!rawValue) continue;

    try {
      const parsedValue = JSON.parse(rawValue);
      const token = findTokenFromObject(parsedValue);

      if (token) return token;
    } catch {
      continue;
    }
  }

  return null;
}

function isFormDataBody(body?: BodyInit | null) {
  return typeof FormData !== "undefined" && body instanceof FormData;
}

function getHeaders(body?: BodyInit | null): HeadersInit {
  const token = getToken();
  const isFormData = isFormDataBody(body);

  return {
    Accept: "application/json",
    ...(!isFormData ? { "Content-Type": "application/json" } : {}),
    ...(token ? { Authorization: `Bearer ${token}` } : {}),
  };
}

function formatApiErrorMessage(value: unknown): string | null {
  if (!value) return null;

  if (typeof value === "string") return value;

  if (Array.isArray(value)) {
    const messages = value
      .map((item) => formatApiErrorMessage(item))
      .filter((item): item is string => Boolean(item));

    return messages.length > 0 ? messages.join("\n") : null;
  }

  if (typeof value === "object") {
    const record = value as Record<string, unknown>;

    return (
      formatApiErrorMessage(record.message) ||
      formatApiErrorMessage(record.error) ||
      formatApiErrorMessage(record.errors)
    );
  }

  return String(value);
}

function findNumberFromObject(value: unknown, keys: string[]): number | null {
  if (!value || typeof value !== "object") return null;

  const record = value as Record<string, unknown>;

  for (const key of keys) {
    const item = record[key];

    if (typeof item === "number" && Number.isFinite(item) && item > 0) {
      return item;
    }

    if (typeof item === "string" && item.trim()) {
      const numberValue = Number(item);

      if (Number.isFinite(numberValue) && numberValue > 0) {
        return numberValue;
      }
    }
  }

  for (const childValue of Object.values(record)) {
    const found = findNumberFromObject(childValue, keys);
    if (found) return found;
  }

  return null;
}

function getCurrentUserId(): number | null {
  if (typeof window === "undefined") return null;

  const directKeys = ["userId", "currentUserId", "id"];

  for (const key of directKeys) {
    const value = localStorage.getItem(key);
    const numberValue = Number(value);

    if (value && Number.isFinite(numberValue) && numberValue > 0) {
      return numberValue;
    }
  }

  const objectKeys = [
    "auth",
    "user",
    "auth-storage",
    "authStore",
    "gpsf-auth-storage",
    "persist:auth",
    "persist:root",
  ];

  for (const key of objectKeys) {
    const rawValue = localStorage.getItem(key);
    if (!rawValue) continue;

    try {
      const parsedValue = JSON.parse(rawValue);
      const userId = findNumberFromObject(parsedValue, [
        "userId",
        "currentUserId",
        "id",
      ]);

      if (userId && userId > 0) return userId;
    } catch {
      continue;
    }
  }

  return null;
}

function normalizeIds(values?: number[]): number[] {
  if (!Array.isArray(values)) return [];

  return Array.from(
    new Set(
      values
        .map((value) => Number(value))
        .filter((value) => Number.isFinite(value) && value > 0),
    ),
  );
}

function normalizeArray<T>(value: unknown): T[] {
  if (Array.isArray(value)) return value as T[];

  if (!value || typeof value !== "object") return [];

  const record = value as Record<string, unknown>;

  const possibleKeys = [
    "data",
    "items",
    "results",
    "records",
    "issues",
    "categories",
    "stakeholders",
    "governmentAgencies",
    "workingGroups",
  ];

  for (const key of possibleKeys) {
    const nestedValue = record[key];

    if (Array.isArray(nestedValue)) {
      return nestedValue as T[];
    }

    if (nestedValue && typeof nestedValue === "object") {
      const nestedArray = normalizeArray<T>(nestedValue);

      if (nestedArray.length > 0) {
        return nestedArray;
      }
    }
  }

  return [];
}

function isApiMeetingRequest(value: unknown): value is ApiMeetingRequest {
  if (!value || typeof value !== "object") return false;

  const id = (value as { id?: unknown }).id;

  return typeof id === "number" && Number.isFinite(id);
}

function extractSentMeetingRequest(
  value: SendMeetingRequestResponse,
): ApiMeetingRequest {
  if (isApiMeetingRequest(value)) {
    return value;
  }

  if (isApiMeetingRequest(value.data)) {
    return value.data;
  }

  if (
    value.data &&
    "data" in value.data &&
    isApiMeetingRequest(value.data.data)
  ) {
    return value.data.data;
  }

  throw new Error("Invalid meeting request response after sending");
}

async function request<T>(
  endpoint: string,
  options: RequestInit = {},
): Promise<T> {
  let response: Response;

  try {
    response = await fetch(`${API_BASE_URL}${endpoint}`, {
      ...options,
      headers: {
        ...getHeaders(options.body),
        ...(options.headers || {}),
      },
      credentials: "include",
      cache: "no-store",
    });
  } catch {
    // fetch only rejects when the request never reached the server, or the
    // connection was dropped before a reply arrived. The browser message for
    // that is "Failed to fetch", which means nothing to the person using the
    // app, so say something they can act on instead.
    throw new Error(
      "Cannot reach the server. Please check your connection and try again.",
    );
  }

  const result = await response.json().catch(() => null);

  if (!response.ok) {
    const message =
      formatApiErrorMessage(result?.message) ||
      formatApiErrorMessage(result?.error) ||
      `Request failed with status ${response.status}`;

    throw new Error(message);
  }

  return result as T;
}

function extractCreatedIssue(data: CreateWorkingGroupIssueResponse): ApiIssue {
  if ("issue" in data && data.issue) {
    return data.issue;
  }

  return data as ApiIssue;
}

// Only issues in the "Saved" status may be attached to a meeting request.
// Prefer the stable code ("SAVED"); fall back to the (editable) name.
function isSavedIssueStatus(status?: ApiIssueStatus | null): boolean {
  if (!status) return false;
  if (status.code) return status.code.trim().toUpperCase() === "SAVED";
  return status.name?.trim().toLowerCase() === "saved";
}

function buildMeetingRequestFormData(
  payload: CreateMeetingRequestPayload | UpdateMeetingRequestPayload,
) {
  const formData = new FormData();

  if (payload.title !== undefined) {
    formData.append("title", payload.title);
  }

  if (payload.description !== undefined) {
    formData.append("description", payload.description);
  }

  const userId =
    payload.userId !== undefined &&
    Number.isFinite(Number(payload.userId)) &&
    Number(payload.userId) > 0
      ? Number(payload.userId)
      : getCurrentUserId() || Number(process.env.NEXT_PUBLIC_CURRENT_USER_ID || "2");

  if (Number.isFinite(userId) && userId > 0) {
    formData.append("userId", String(userId));
  }

  if (payload.meetingRequestLetter !== undefined) {
    formData.append("meetingRequestLetter", payload.meetingRequestLetter);
  }

  const stakeholderIds = normalizeIds(payload.stakeholderIds);

  stakeholderIds.forEach((stakeholderId) => {
    formData.append("stakeholderIds", String(stakeholderId));
  });

  const issueIds = normalizeIds(payload.issueIds);

  issueIds.forEach((issueId) => {
    formData.append("issueIds", String(issueId));
  });

  return formData;
}

export const meetingRequestService = {
  async getGovernmentAgencies(): Promise<ApiGovernmentAgency[]> {
    const response = await request<ApiResponse<unknown>>(
      "/working-group-issues/government-agencies",
      {
        method: "GET",
      },
    );

    return normalizeArray<ApiGovernmentAgency>(response.data).map(
      normalizeGovernmentAgency,
    );
  },

  async getWorkingGroups(): Promise<ApiWorkingGroup[]> {
    const response = await request<ApiResponse<unknown>>(
      "/stakeholders/working-groups",
      {
        method: "GET",
      },
    );

    return normalizeArray<ApiWorkingGroup>(response.data)
      .filter((item) => item.active !== false && !item.deletedAt)
      .map(normalizeStakeholder);
  },

  async getMyPrimaryGovernment(): Promise<ApiPrimaryGovernment> {
    let response: ApiResponse<ApiPrimaryGovernment>;

    try {
      response = await request<ApiResponse<ApiPrimaryGovernment>>(
        "/working-group-issues/my/primary-government",
        {
          method: "GET",
        },
      );
    } catch {
      response = await request<ApiResponse<ApiPrimaryGovernment>>(
        "/working-group-issues/my/primary-goverment",
        {
          method: "GET",
        },
      );
    }

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

  async getStakeholderById(id: number): Promise<ApiStakeholder> {
    const response = await request<ApiResponse<ApiStakeholder>>(
      `/stakeholders/${id}`,
      {
        method: "GET",
      },
    );

    return normalizeStakeholder(response.data);
  },

  async getWorkingGroupIssues(): Promise<ApiIssue[]> {
    const response = await request<ApiResponse<unknown>>(
      "/working-group-issues",
      {
        method: "GET",
      },
    );

    const allIssues = normalizeArray<ApiIssue>(response.data);

    return allIssues.filter((issue) => {
      if (issue.deletedAt) return false;

      // Issue submitted/sent with meeting request already => hide from Issue List.
      // New issue that is not submitted yet will still show.
      if (issue.meetingRequestId && Number(issue.meetingRequestId) > 0) {
        return false;
      }

      // Only "Saved" issues can be attached to a meeting request.
      if (!isSavedIssueStatus(issue.issueStatus)) {
        return false;
      }

      return true;
    });
  },

  async getCategories(): Promise<ApiCategory[]> {
    const response = await request<ApiResponse<unknown>>(
      "/working-group-issues/categories",
      {
        method: "GET",
      },
    );

    return normalizeArray<ApiCategory>(response.data);
  },

  async createWorkingGroupIssue(
    payload: CreateWorkingGroupIssuePayload,
  ): Promise<ApiIssue> {
    const formData = new FormData();

    formData.append("title", payload.title);
    formData.append("description", payload.description);
    formData.append("recommendation", payload.recommendation);
    formData.append("categoryId", String(payload.categoryId));
    formData.append("issueStatusId", String(payload.issueStatusId));

    formData.append(
      "governmentAgencies",
      JSON.stringify(
        payload.stakeholderIds.map((stakeholderId, index) => ({
          stakeholderId,
          agencyOrder: index + 1,
        })),
      ),
    );

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

    const response = await request<ApiResponse<CreateWorkingGroupIssueResponse>>(
      "/working-group-issues",
      {
        method: "POST",
        body: formData,
      },
    );

    return extractCreatedIssue(response.data);
  },

  async getAll(): Promise<ApiMeetingRequest[]> {
    const response = await request<ApiResponse<unknown>>("/meeting-requests", {
      method: "GET",
    });

    return normalizeArray<ApiMeetingRequest>(response.data).map(
      normalizeMeetingRequest,
    );
  },

  async getById(id: number): Promise<ApiMeetingRequest> {
    const response = await request<ApiResponse<ApiMeetingRequest>>(
      `/meeting-requests/${id}`,
      {
        method: "GET",
      },
    );

    return normalizeMeetingRequest(response.data);
  },

  async create(
    payload: CreateMeetingRequestPayload,
  ): Promise<ApiMeetingRequest> {
    const formData = buildMeetingRequestFormData(payload);

    const response = await request<ApiResponse<ApiMeetingRequest>>(
      "/meeting-requests",
      {
        method: "POST",
        body: formData,
      },
    );

    return normalizeMeetingRequest(response.data);
  },

  async update(
    id: number,
    payload: UpdateMeetingRequestPayload,
  ): Promise<ApiMeetingRequest> {
    const formData = buildMeetingRequestFormData(payload);

    const response = await request<ApiResponse<ApiMeetingRequest>>(
      `/meeting-requests/${id}`,
      {
        method: "PATCH",
        body: formData,
      },
    );

    return normalizeMeetingRequest(response.data);
  },

  async sendRequest(id: number): Promise<ApiMeetingRequest> {
    const response = await request<ApiResponse<SendMeetingRequestResponse>>(
      `/meeting-requests/${id}/send-request`,
      {
        method: "PATCH",
      },
    );

    return normalizeMeetingRequest(extractSentMeetingRequest(response.data));
  },

  async delete(id: number): Promise<boolean> {
    await request<ApiResponse<null>>(`/meeting-requests/${id}`, {
      method: "DELETE",
    });

    return true;
  },
};
