import { AxiosResponse } from 'axios';
import { compareAsc } from 'date-fns';
import render from 'dom-serializer';
import { parseDocument } from 'htmlparser2';

import { FileExtensions } from 'src/constants/filesPreview';
import { EventName } from 'src/types/amplitude';
import { type Attachment, FileTypes, GIPHY } from 'src/types/attachments';
import { GroupPost } from 'src/types/groups';
import { type Post, type PostData, PostSegmentation } from 'src/types/posts';
import { type LinkPreviewResponse } from 'src/types/previewLink';
import { SegmentationMapValues } from 'src/types/segmentation';
import { CursorPagination } from 'src/types/services';
import { TaggedUser } from 'src/types/user';
import { getDownloadName } from 'src/utils/files';
import {
  hasAllPermissions,
  hasAnyPermissions,
  UserPermissions,
} from 'src/utils/permissions';
import { zeroWidthSpaceRegex } from 'src/utils/validation';

import { getProfileDetailHref } from 'src/components/RichTextEditor/mentions/utils';
import { NodeTypes } from 'src/components/RichTextEditor/types';

import { urlRegex } from './previewLink';

export const getNextScheduledPost = (posts: Post[]): Post =>
  posts.reduce((min, current) => {
    const minDate = new Date(min.publicationDatetime);
    const currentDate = new Date(current.publicationDatetime);
    if (compareAsc(currentDate, minDate) < 0) {
      return current;
    }

    return min;
  }, posts[0]);

export const findPost = (id: number, dataArray: Post[]) => {
  const postIndex = dataArray.findIndex(post => post.id === id);
  if (postIndex !== -1) {
    return dataArray[postIndex];
  }
  return null;
};

export const findPostIndex = (id: number, dataArray: Post[]) =>
  dataArray.findIndex(post => post.id === id);

export const findPostOnPages = (
  id: number,
  pages: AxiosResponse<CursorPagination<Post[]>>[],
) => {
  for (let i = 0; i < pages.length; i++) {
    const postIndex = pages[i].data.items.findIndex(post => post.id === id);
    if (postIndex !== -1) return pages[i].data.items[postIndex];
  }

  return null;
};

export const findPostIndexOnPages = (
  id: number,
  pages: any[],
): [pageIndex: number, postIndex: number] => {
  if (!Array.isArray(pages)) {
    return [-1, -1];
  }

  for (let i = 0; i < pages.length; i++) {
    // pages[i].data can be CursorPagination<T> or T[]
    const postIndex = (pages[i].data.items || pages[i].data).findIndex(
      (post: { id: number }) => post.id === id,
    );
    if (postIndex !== -1) return [i, postIndex];
  }

  return [-1, -1];
};

export const canDelete = (
  loggedUserId: number,
  postUserId: number,
  permissions: string[],
): boolean =>
  hasAnyPermissions(permissions, [UserPermissions.MANAGE_POSTS]) ||
  loggedUserId === postUserId;

export const canEdit = (loggedUserId: number, postUserId: number): boolean =>
  loggedUserId === postUserId;

export const canPinPost = (permissions: string[]): boolean =>
  hasAnyPermissions(permissions, [UserPermissions.MANAGE_POSTS]);

export const canComment = (permissions: string[]): boolean =>
  hasAnyPermissions(permissions, [
    UserPermissions.CREATE_COMMENT,
    UserPermissions.CREATE_COMMENT_IN_POSTS,
  ]);

export const canReactContent = (permissions: string[]): boolean =>
  hasAnyPermissions(permissions, [
    UserPermissions.CAN_REACT_CONTENT,
    UserPermissions.CAN_REACT_POSTS,
  ]);

export const canApprovePosts = (permissions: string[]): boolean =>
  hasAnyPermissions(permissions, [UserPermissions.CAN_APPROVE_FEED_POSTS]);

export const isPostApprovalRequested = (permissions: string[]): boolean =>
  hasAnyPermissions(permissions, [UserPermissions.REQUEST_FEED_POST_APPROVAL]);

export const canMarkAsKeyUpdate = (permissions: string[]): boolean =>
  hasAllPermissions(permissions, [
    UserPermissions.CREATE_KEY_UPDATES,
    UserPermissions.VIEW_KEY_UPDATES,
  ]);

export const addVote = <PostType extends Post | GroupPost>(
  post: PostType | null,
  optionId: number,
  isLoggedUserAnswer: boolean = true,
): PostType | null => {
  if (!post?.poll) return post;

  post.poll.totalAnswerCount++;
  post.poll.hasBeenAnswered = post.poll.hasBeenAnswered || isLoggedUserAnswer;
  post.poll.pollOptions.forEach(option => {
    if (option?.id === optionId) {
      option.answerCount++;
      option.isLoggedUserAnswer = isLoggedUserAnswer;
      return;
    }
  });

  return post;
};

export const getEventNameArticle = (
  eventNameAmplitude?: EventName | null,
): EventName | null => {
  if (eventNameAmplitude) return eventNameAmplitude;
  return null;
};

export const isPostPinned = (
  pinPostData: (Post | GroupPost)[],
  post: Post | GroupPost,
) => {
  if (!Array.isArray(pinPostData) || !post) return false;

  return pinPostData.some(pin => pin.id === post.id);
};

/**
 * Gets the last (most recently) pinned post from the array
 */
export const getLastPinnedPost = <T extends { createdAt: string; id: number }>(
  pinnedPosts: T[],
): T | null => {
  if (!pinnedPosts || pinnedPosts.length === 0) return null;

  const sorted = [...pinnedPosts].sort((a, b) => {
    const dateA = new Date(a.createdAt).getTime();
    const dateB = new Date(b.createdAt).getTime();
    return dateB - dateA;
  });

  return sorted[0];
};

export const getSegmentationMap = (segmentation: PostSegmentation[]) => {
  const newSegmentations = new Map();
  if (segmentation) {
    segmentation.forEach(segmentationItem => {
      if (!newSegmentations.has(segmentationItem.item.groupId)) {
        newSegmentations.set(segmentationItem.item.groupId, []);
      }
      newSegmentations.get(segmentationItem.item.groupId).push({
        id: segmentationItem.item.id,
        name: segmentationItem.item.name,
      });
    });
  }
  return newSegmentations;
};

export const getSegmentationArray = (
  segmentationItemIds: Map<number, SegmentationMapValues[]>,
) => {
  let strValues = null;

  const mapValues = Array.from(segmentationItemIds.values()).map(itemArray =>
    itemArray.map(({ id }) => id),
  );

  if (mapValues.length > 0) {
    const arrayValues = mapValues.reduce((prev, next) => prev.concat(next));
    strValues = arrayValues.join(',');
  }

  return strValues;
};

export const getSelectedGroups = (
  segmentationItemIds: Map<number, SegmentationMapValues[]>,
) =>
  Array.from(segmentationItemIds.values()).map(itemArray =>
    itemArray.map(({ id, name }) => [id, name]),
  );

export const removeElementFromMapArray = (
  arr: SegmentationMapValues[],
  idValue: number,
) => arr.filter(item => item.id !== idValue);

export const getArrayOfMapValues = (
  segmentation: Map<number, SegmentationMapValues[]>,
) =>
  Array.from(segmentation.values()).map(itemArray =>
    itemArray.map(({ id }) => id),
  );

export const getArrayFromTwoDimensionArray = (doubleArray: number[][]) =>
  doubleArray.reduce((prev, next) => prev.concat(next), [] as number[]);

export const getKey = (
  segmentation: Map<number, SegmentationMapValues[]>,
  idValue: number,
) => {
  let keyValue = null;
  [...segmentation].forEach(([key, val]) => {
    val.forEach(item => {
      if (item.id === idValue) {
        keyValue = key;
      }
    });
  });
  return keyValue;
};

export const createGifBody = (gif: any) => {
  if (!gif) return;

  const { height, width } = gif.images?.original || {
    height: null,
    width: null,
  };

  return {
    type: FileTypes.GIF,
    externalReference: {
      id: gif.id,
      origin: GIPHY,
    },
    height,
    width,
  };
};

export const buildPostData = ({
  body,
  bodyHtml,
  taggedUsers,
  segmentationItems,
  dateTimeSchedule,
  uploadImages,
  uploadFiles,
  uploadVideos,
  uploadAudios = [],
  sendNotification,
  gif,
  withPoll,
  pollOptions,
  pollVotersVisible,
  pollEndsAt,
  linkPreviews,
}: {
  body: string;
  bodyHtml: string;
  taggedUsers: TaggedUser[];
  segmentationItems: Map<number, SegmentationMapValues[]>;
  dateTimeSchedule?: string | null;
  uploadImages: Attachment[];
  uploadFiles: Attachment[];
  uploadVideos: Attachment[];
  uploadAudios?: Attachment[];
  sendNotification?: boolean;
  gif?: unknown;
  withPoll?: boolean;
  pollOptions?: string[];
  pollVotersVisible?: boolean;
  pollEndsAt?: string | null;
  linkPreviews?: Record<string, LinkPreviewResponse | null>;
}): PostData => {
  const mapSegmentationItemsValues = getArrayOfMapValues(segmentationItems);
  const arraySegmentationItemsValues = getArrayFromTwoDimensionArray(
    mapSegmentationItemsValues,
  );
  const taggedUserIds = taggedUsers.map(
    (attribute: TaggedUser) => attribute.type.user.id,
  );

  const gifBody = createGifBody(gif);

  return {
    body,
    bodyHtml,
    bodyAttributes: taggedUsers,
    linkPreviews,
    segmentationItemIds: arraySegmentationItemsValues,
    publicationDatetime: dateTimeSchedule,
    sendNotification: sendNotification ?? false,
    attachments: [
      gifBody,
      ...uploadImages,
      ...uploadFiles,
      ...uploadVideos,
      ...uploadAudios,
    ].filter((attachment): attachment is Attachment => Boolean(attachment)),
    ...(taggedUserIds.length > 0 && { taggedUserIds }),
    ...(withPoll && {
      poll: {
        pollOptions: pollOptions ?? [],
        pollConfiguration: {
          isAnonymous: !pollVotersVisible,
          ...(pollEndsAt && { endsAt: pollEndsAt }),
        },
      },
    }),
  };
};

export const getDownloadReportName = (
  type: string,
  extension: string = FileExtensions.XLSX,
) => {
  return getDownloadName(`humand-${type}-`, `.${extension}`);
};

export const transformTagInStrongElement = (
  body: string,
  taggedUsers: TaggedUser[],
) => {
  let textStart;
  let textTaggedUser;
  let textEnd;
  let bodyFormatted = body;

  const taggedUsersSorted = taggedUsers.sort((a, b) =>
    a.start > b.start ? 1 : -1,
  );

  const amountCaractersInSpan = 156;

  if (!taggedUsersSorted.length || !body) {
    return bodyFormatted;
  } else {
    for (let i = 0; i < taggedUsersSorted.length; i++) {
      const element = taggedUsersSorted[i];
      textStart = bodyFormatted.slice(
        0,
        element.start + amountCaractersInSpan * i,
      );
      textTaggedUser = bodyFormatted.slice(
        element.start + amountCaractersInSpan * i,
        element.start + element.length + amountCaractersInSpan * i,
      );
      textEnd = bodyFormatted.slice(
        amountCaractersInSpan * i + element.start + element.length,
      );
      bodyFormatted =
        textStart +
        '<span style="z-index:1; pointer-events: none; opacity:0.7; position: relative; color:#1976D2; background-color: #E6F3FA; text-decoration:underline;">' +
        textTaggedUser +
        '</span>' +
        textEnd;
    }
  }

  return bodyFormatted;
};

export const withoutNonPrintingCharacter = (text: string): string =>
  text.replace(zeroWidthSpaceRegex, '');

export const truncateHTML = (
  html: string,
  maxLength: number,
  maxLines: number,
) => {
  if (!html) return { htmlTruncated: '', isTruncated: false };

  const document = parseDocument(html);
  let currentLength = 0;
  let currentLines = 0;
  let isTruncated = false;

  const traverseAndTruncate = (node: any) => {
    if (isTruncated) {
      return null;
    }

    // Increment line count for each block element
    if (
      node.type === NodeTypes.TAG &&
      ['p', 'div', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li'].includes(
        node.name,
      )
    ) {
      currentLines++;
    }

    // Stop if the line limit is reached
    if (currentLines > maxLines) {
      isTruncated = true;
      // Add ellipsis to the end of the last line
      return {
        type: NodeTypes.TEXT,
        data: '...',
      };
    }

    // Process text nodes and check character limit
    if (node.type === NodeTypes.TEXT) {
      const text = node.data;

      // Stop if the character limit is reached
      if (currentLength + text.length > maxLength) {
        const remainingChars = maxLength - currentLength;
        const cutIndex = text.lastIndexOf(' ', remainingChars);

        node.data = `${text.slice(0, cutIndex >= 0 ? cutIndex : remainingChars)}...`;
        isTruncated = true;
      } else {
        currentLength += text.length;
      }
    } else if (node.children?.length) {
      // Traverses and truncates child elements, without removing empty nodes
      node.children = node.children
        .map(traverseAndTruncate)
        .filter((child: any) => child);

      // Return null if there are no children left
      if (node.children.length === 0) {
        return null;
      }
    }

    return node;
  };

  document.children = document.children
    .map(traverseAndTruncate)
    .filter(
      child =>
        child && (child.type !== NodeTypes.TEXT || child.data.trim() !== ''),
    );

  return {
    htmlTruncated: render(document),
    isTruncated,
  };
};

// Helper function to get text from a node and all its children
const getTextFromNode = (node: any): string => {
  if (node.type === NodeTypes.TEXT) return node.data;
  return (node.children || []).map(getTextFromNode).join('');
};

// TODO: Workarround until v2 for extract the id user from data-id instead of href
const extractIdFromHref = (href: string) => {
  const match = href.match(/\/profile\/(\d+)/);
  return match ? parseInt(match[1], 10) : null;
};

export const extractTextFromHTML = (html: string) => {
  const document = parseDocument(html);
  let plainText = '';
  const mentions: {
    type: { user: { id: number | null } };
    start: number;
    length: number;
  }[] = [];
  let hasRichText = false;

  const traverseNodes = (node: any) => {
    if (node.type === NodeTypes.TEXT) {
      plainText += node.data;
    } else if (node.name === 'a' && node.attribs?.href) {
      const linkText = getTextFromNode(node);
      const href = node.attribs.href;
      const userId = extractIdFromHref(href);
      if (node.attribs.class === 'mention') {
        mentions.push({
          type: { user: { id: userId } },
          start: plainText.length,
          length: linkText.length,
        });
        plainText += linkText;
      } else {
        if (linkText === href) {
          plainText += href;
        } else {
          plainText += `${linkText} ( ${href} )`;
        }
      }
    } else if (node.name === 'li') {
      plainText += '- ';
      node.children.forEach(traverseNodes);
    } else if (node.children) {
      node.children.forEach(traverseNodes);
    }

    if (['p', 'h1', 'h2', 'h3', 'h4'].includes(node.name)) {
      plainText += '\n\n';
    } else if (node.name === 'br') {
      plainText += '\n';
    }

    if (!hasRichText && isRichTextNode(node)) {
      hasRichText = true;
    }
  };

  document.children.forEach(traverseNodes);
  return { plainText: plainText.trim(), mentions, hasRichText };
};

/**
 * Plain text length aligned with TipTap's CharacterCount (no extra block separators
 * or URL decoration like {@link extractTextFromHTML}).
 */
export const getPlainTextForCharacterCount = (html: string): string => {
  const document = parseDocument(html);
  let text = '';

  const traverse = (node: any) => {
    // Count mention atoms as a single character to align with TipTap's CharacterCount,
    // which uses leafText = ' ' for atom nodes (e.g., Mention).
    if (node?.name === 'a' && node?.attribs?.class === 'mention') {
      text += ' ';
      return;
    }
    if (node?.type === NodeTypes.TEXT) {
      text += node.data;
      return;
    }
    if (node?.children?.length) {
      node.children.forEach(traverse);
    }
  };

  document.children.forEach(traverse);
  return text;
};

/**
 * Finds the first occurrence of matchText in text (case-insensitive)
 * Shared logic used by both showMatch and highlightTextInHTML
 * @param text - The text to search in
 * @param matchText - The text to find (case-insensitive)
 * @returns Object with matchIndex and matchLength if found, null otherwise
 */
export const findFirstMatch = (
  text: string,
  matchText: string,
): { matchIndex: number; matchLength: number } | null => {
  if (!matchText || !text) return null;

  const matchIndex = text.toLowerCase().indexOf(matchText.toLowerCase());
  if (matchIndex === -1) return null;

  return { matchIndex, matchLength: matchText.length };
};

/**
 * Highlights matching text in HTML content by wrapping matches in a highlight mark
 * Uses the same logic as showMatch in ParagraphsTextType - simple first match only
 * @param html - The HTML content to highlight
 * @param matchText - The text to highlight (case-insensitive)
 * @returns HTML with highlighted matches wrapped in <mark> tags with background color
 */
export const highlightTextInHTML = (
  html: string,
  matchText: string,
): string => {
  if (!matchText || !html) return html;

  // Extract plain text to check if match exists (same logic as showMatch)
  const { plainText } = extractTextFromHTML(html);
  const hasMatch = findFirstMatch(plainText, matchText) !== null;

  if (!hasMatch) return html;

  // Parse HTML and traverse text nodes to find and highlight the first match
  const document = parseDocument(html);
  let foundMatch = false;

  const highlightTextNode = (node: any): void => {
    if (foundMatch) return; // Only highlight first match

    if (node.type === NodeTypes.TEXT && node.data) {
      const text = node.data;
      const matchInfo = findFirstMatch(text, matchText);

      if (matchInfo) {
        foundMatch = true;
        const { matchIndex, matchLength } = matchInfo;

        // Split the text node: before match, match, after match (same as showMatch)
        const beforeMatch = text.slice(0, matchIndex);
        const match = text.slice(matchIndex, matchIndex + matchLength);
        const afterMatch = text.slice(matchIndex + matchLength);

        // Create replacement nodes
        const replacementNodes: any[] = [];

        if (beforeMatch) {
          replacementNodes.push({
            type: NodeTypes.TEXT,
            data: beforeMatch,
          });
        }

        // Create mark element for the highlight (same style as showMatch)
        replacementNodes.push({
          type: NodeTypes.TAG,
          name: 'mark',
          attribs: {
            style: 'background-color: #f9e8b4;',
          },
          children: [
            {
              type: NodeTypes.TEXT,
              data: match,
            },
          ],
        });

        if (afterMatch) {
          replacementNodes.push({
            type: NodeTypes.TEXT,
            data: afterMatch,
          });
        }

        // Replace the current text node with the highlighted nodes
        if (node.parent && node.parent.children) {
          const index = node.parent.children.indexOf(node);
          if (index !== -1) {
            node.parent.children.splice(index, 1, ...replacementNodes);
            // Set parent reference for new nodes
            replacementNodes.forEach((child: any) => {
              child.parent = node.parent;
            });
          }
        }
      }
    } else if (node.children && !foundMatch) {
      // Recursively process children
      [...node.children].forEach(highlightTextNode);
    }
  };

  document.children.forEach(highlightTextNode);

  return foundMatch ? render(document.children) : html;
};

const isRichTextNode = (node: any) =>
  [
    'u',
    'b',
    'i',
    's',
    'h1',
    'h2',
    'h3',
    'h4',
    'li',
    'ul',
    'ol',
    'em',
    'code',
    'strong',
    'blockquote',
  ].includes(node.name) ||
  (node.name === 'a' && node.attribs.class !== 'mention');

const replaceMentions = (
  paragraph: string,
  mentions: { mentionText: string; id: number }[],
) => {
  mentions.forEach(
    ({ mentionText, id }: { mentionText: string; id: number }) => {
      if (paragraph.includes(mentionText)) {
        const href = getProfileDetailHref(id);
        const mentionHtml = `<a href=\"${href}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"mention\" data-id=\"${id}\" data-type=\"mention\" data-label=${mentionText}>${mentionText}</a>`;
        paragraph = paragraph.replace(mentionText, mentionHtml);
      }
    },
  );
  return `<p> ${paragraph} </p>`;
};

export const generateHTMLFromText = (
  body: string,
  bodyAttributes: TaggedUser[],
) => {
  const mentions = bodyAttributes
    .filter((attr: TaggedUser) => attr.type.user)
    .map((attr: TaggedUser) => ({
      mentionText: body.slice(attr.start, attr.start + attr.length),
      id: attr.type.user.id,
    }));

  body = body.replace(urlRegex, (url: string) => {
    return `<a target="_blank" rel="noopener noreferrer" href="${url}">${url}</a>`;
  });

  // Convert \n in <p> tags
  const paragraphs = body.split(/\n+/).map((paragraph: string) => {
    if (paragraph.includes('@')) {
      return replaceMentions(paragraph, mentions);
    } else {
      return `<p>${paragraph}</p>`;
    }
  });

  return paragraphs.join('');
};

export const getCorrectLiveDescription = (
  t: (key: string) => string,
  isRoot: boolean,
  isLive: boolean,
) => {
  if (isRoot) {
    return isLive
      ? t('post:live_stream_description')
      : t('post:post_live_stream_description');
  }
  return isLive
    ? t('post:live_stream_description_group')
    : t('post:post_live_stream_description_group');
};

export const getDeleteModalTexts = ({
  isComment,
  isLive,
  t,
}: {
  isComment: boolean;
  isLive: boolean;
  t: (key: string) => string | undefined;
}) => {
  let title = t('general:confirmation') ?? '';
  if (!isComment && isLive)
    title = t('post:confirm_delete_live_post_title') ?? '';

  let textBody = t('post:confirm_delete_post') ?? '';
  if (isComment) textBody = t('comments.confirm_delete_comment') ?? '';
  else if (isLive) textBody = t('post:confirm_delete_live_post') ?? '';

  return { title, textBody };
};
