export type ChatComposerAttachment = {
  id: string;
  name: string;
  path: string;
  publicUrl: string;
  size: number;
  mimeType: string;
};

export type ChatMessagePart =
  | {
      type: 'link_card';
      title: string;
      url: string;
      actionLabel?: string | null;
    }
  | {
      type: 'prompt';
      title: string;
      description?: string | null;
      submitLabel?: string | null;
      mode?: 'queue' | 'direct' | null;
      multiple?: boolean;
      responseTemplate?: string | null;
      freeTextLabel?: string | null;
      freeTextPlaceholder?: string | null;
      currentStepKey?: string | null;
      steps?: Array<{
        key: string;
        title: string;
        description?: string | null;
        submitLabel?: string | null;
        mode?: 'queue' | 'direct' | null;
        multiple?: boolean;
        optional?: boolean;
        responseTemplate?: string | null;
        freeTextLabel?: string | null;
        freeTextPlaceholder?: string | null;
        selectedValues?: string[];
        options: Array<{
          value: string;
          label: string;
          description?: string | null;
          preview?:
            | {
                type: 'image' | 'markdown' | 'html' | 'resource';
                url?: string | null;
                content?: string | null;
                alt?: string | null;
                title?: string | null;
              }
            | null;
        }>;
      }>;
      readOnly?: boolean;
      selectedValues?: string[];
      resolvedBy?: 'user' | 'timeout' | 'system' | null;
      resolvedAt?: string | null;
      resultText?: string | null;
      attachments?: ChatComposerAttachment[];
      options: Array<{
        value: string;
        label: string;
        description?: string | null;
        preview?:
          | {
              type: 'image' | 'markdown' | 'html' | 'resource';
              url?: string | null;
              content?: string | null;
              alt?: string | null;
              title?: string | null;
            }
          | null;
      }>;
    };

type PromptPart = Extract<ChatMessagePart, { type: 'prompt' }>;
type PromptOption = PromptPart['options'][number];
type PromptPreview = NonNullable<PromptOption['preview']>;
type PromptStep = NonNullable<PromptPart['steps']>[number];

const LINK_CARD_LINE_PATTERN = /^\s*\[\[link-card:(.+?)\]\]\s*$/i;
const PROMPT_LINE_PATTERN = /^\s*\[\[prompt:(.+?)\]\]\s*$/i;
const STANDALONE_MARKDOWN_LINK_LINE_PATTERN = /^\s*(?:[-*+]\s+|\d+\.\s+)?\[([^\]]+)\]\((.+)\)\s*$/;
const STANDALONE_URL_LINE_PATTERN = /^\s*(?:[-*+]\s+|\d+\.\s+)?((?:https?:\/\/|\/)[^\s<>)\]]+)\s*$/i;
const RESOURCE_PATH_PREFIXES = ['/api/chat/resources/', '/public/.codex_chat/', '/.codex_chat/'] as const;
const CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
const CHAT_DOT_CODEX_MARKER = '/.codex_chat/';
const CHAT_PUBLIC_DOT_CODEX_MARKER = '/public/.codex_chat/';
const RESOURCE_MANAGER_PREVIEW_MARKER = '/api/resource-manager/preview/';
const RESOURCE_MANAGER_ROOT_MARKER = 'resource/';

function buildCanonicalChatApiResourcePath(relativePath: string) {
  const normalizedRelativePath = normalizeText(relativePath).replace(/^\/+/, '');

  if (!normalizedRelativePath) {
    return '';
  }

  if (normalizedRelativePath.startsWith('.codex_chat/')) {
    return `${CHAT_API_RESOURCE_MARKER}${normalizedRelativePath}`;
  }

  return `${CHAT_API_RESOURCE_MARKER}.codex_chat/${normalizedRelativePath}`;
}

function normalizeText(value: unknown) {
  return String(value ?? '').trim();
}

function unwrapMarkdownLinkTarget(value: string) {
  const normalized = normalizeText(value);

  if (!normalized) {
    return '';
  }

  const matched = normalized.match(/^<([\s\S]+)>$/);
  return matched?.[1]?.trim() ?? normalized;
}

function normalizeResourceManagerPathSegment(segment: string) {
  const normalized = normalizeText(segment);

  if (!normalized) {
    return '';
  }

  try {
    return encodeURIComponent(decodeURIComponent(normalized));
  } catch {
    return encodeURIComponent(normalized);
  }
}

function normalizeUrlFragmentValue(value: string) {
  const normalized = normalizeText(value).replace(/^#+/, '');

  if (!normalized) {
    return '';
  }

  try {
    return decodeURIComponent(normalized);
  } catch {
    return normalized;
  }
}

function decodeRepeatedly(value: string, maxIterations = 3) {
  let current = value;

  for (let index = 0; index < maxIterations; index += 1) {
    try {
      const decoded = decodeURIComponent(current);

      if (!decoded || decoded === current) {
        break;
      }

      current = decoded;
    } catch {
      break;
    }
  }

  return current;
}

function normalizePreviewPathHash(value: string) {
  const normalized = normalizeText(value);

  if (!normalized) {
    return '';
  }

  const isAbsoluteUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized);
  const isRootRelative = normalized.startsWith('/');

  try {
    const parsed = new URL(normalized, 'https://local.invalid');
    const segments = parsed.pathname.split('/');
    const lastSegment = segments.at(-1) ?? '';
    const decodedLastSegment = decodeRepeatedly(lastSegment);
    const hashIndex = decodedLastSegment.lastIndexOf('#');

    if (hashIndex <= 0 || parsed.hash) {
      return normalized;
    }

    const fileName = decodedLastSegment.slice(0, hashIndex).trim();
    const fragment = normalizeUrlFragmentValue(decodedLastSegment.slice(hashIndex + 1));

    if (!fileName || !fragment || !/\.[a-z0-9]{1,16}$/i.test(fileName)) {
      return normalized;
    }

    segments[segments.length - 1] = encodeURIComponent(fileName);
    parsed.pathname = segments.join('/');
    parsed.hash = fragment;

    if (isAbsoluteUrl) {
      return parsed.toString();
    }

    const pathWithQueryAndHash = `${parsed.pathname}${parsed.search}${parsed.hash}`;
    return isRootRelative ? pathWithQueryAndHash : pathWithQueryAndHash.replace(/^\/+/, '');
  } catch {
    return normalized;
  }
}

function buildResourceManagerPreviewUrl(value: string) {
  const normalized = normalizePreviewPathHash(normalizeText(value).replace(/\\/g, '/'));
  const hashIndex = normalized.indexOf('#');
  const hash = hashIndex >= 0 ? normalizeUrlFragmentValue(normalized.slice(hashIndex + 1)) : '';
  const normalizedPath = hashIndex >= 0 ? normalized.slice(0, hashIndex) : normalized;
  const matchedResourcePath = normalizedPath.match(/(?:^|\/)(resource\/.+)$/i)?.[1];
  const resourcePath = normalizeText(matchedResourcePath).replace(/^\/+/, '');

  if (!resourcePath) {
    return '';
  }

  const relativePath = resourcePath.slice(RESOURCE_MANAGER_ROOT_MARKER.length).replace(/^\/+/, '');

  if (!relativePath) {
    return '';
  }

  const encodedPath = relativePath
    .split('/')
    .filter(Boolean)
    .map((segment) => normalizeResourceManagerPathSegment(segment))
    .join('/');

  return encodedPath ? `${RESOURCE_MANAGER_PREVIEW_MARKER}${encodedPath}${hash ? `#${hash}` : ''}` : '';
}

function normalizeUrl(value: string) {
  const normalized = unwrapMarkdownLinkTarget(value);

  if (!normalized) {
    return '';
  }

  try {
    const parsed = new URL(normalized, 'https://local.invalid');
    const pathname = `${parsed.pathname || ''}${parsed.search || ''}${parsed.hash || ''}`;

    if (pathname.startsWith(CHAT_API_RESOURCE_MARKER) || pathname.startsWith(RESOURCE_MANAGER_PREVIEW_MARKER)) {
      return normalizePreviewPathHash(pathname);
    }

    if (pathname.startsWith(CHAT_PUBLIC_DOT_CODEX_MARKER)) {
      return buildCanonicalChatApiResourcePath(pathname.slice(CHAT_PUBLIC_DOT_CODEX_MARKER.length));
    }

    if (pathname.startsWith(CHAT_DOT_CODEX_MARKER)) {
      return buildCanonicalChatApiResourcePath(pathname.slice(CHAT_DOT_CODEX_MARKER.length));
    }
  } catch {
    // Fall through to handle relative and embedded resource paths below.
  }

  const malformedResourceMatch = normalized.match(/^https?:\/(api\/chat\/resources\/.+)$/i);
  if (malformedResourceMatch?.[1]) {
    return `/${malformedResourceMatch[1]}`;
  }

  const apiMarkerIndex = normalized.lastIndexOf(CHAT_API_RESOURCE_MARKER);
  if (apiMarkerIndex >= 0) {
    const apiPath = normalized.slice(apiMarkerIndex);
    const dotCodexIndex = apiPath.indexOf(CHAT_DOT_CODEX_MARKER);
    return dotCodexIndex >= 0
      ? buildCanonicalChatApiResourcePath(apiPath.slice(dotCodexIndex + 1))
      : apiPath;
  }

  const publicDotCodexIndex = normalized.lastIndexOf(CHAT_PUBLIC_DOT_CODEX_MARKER);
  if (publicDotCodexIndex >= 0) {
    return buildCanonicalChatApiResourcePath(normalized.slice(publicDotCodexIndex + 8));
  }

  const dotCodexIndex = normalized.lastIndexOf(CHAT_DOT_CODEX_MARKER);
  if (dotCodexIndex >= 0) {
    return buildCanonicalChatApiResourcePath(normalized.slice(dotCodexIndex + 1));
  }

  if (normalized === 'resource' || normalized === '/resource' || normalized.startsWith('resource/') || normalized.includes('/resource/')) {
    const resourceManagerPreviewUrl = buildResourceManagerPreviewUrl(normalized);

    if (resourceManagerPreviewUrl) {
      return resourceManagerPreviewUrl;
    }
  }

  if (/^(?:https?:\/\/|\/)/i.test(normalized)) {
    return normalized;
  }

  return '';
}

function normalizePromptPreview(value: unknown): PromptPreview | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }

  const record = value as Record<string, unknown>;
  const type: 'image' | 'markdown' | 'html' | 'resource' | null =
    record.type === 'image' || record.type === 'markdown' || record.type === 'html' || record.type === 'resource'
      ? record.type
      : null;
  const url = normalizeUrl(normalizeText(record.url));
  const content = String(record.content ?? '').trim() || null;
  const alt = normalizeText(record.alt) || null;
  const title = normalizeText(record.title) || null;

  if (!type) {
    return null;
  }

  if (type === 'image' || type === 'resource') {
    if (!url) {
      return null;
    }
  } else if (!content && !url) {
    return null;
  }

  return {
    type,
    url: url || null,
    content,
    alt,
    title,
  };
}

function normalizePromptOption(value: unknown): PromptOption | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }

  const record = value as Record<string, unknown>;
  const optionValue = normalizeText(record.value);
  const label = normalizeText(record.label);

  if (!optionValue || !label) {
    return null;
  }

  return {
    value: optionValue,
    label,
    description: normalizeText(record.description) || null,
    preview: normalizePromptPreview(record.preview),
  };
}

function normalizePromptSelectedValues(value: unknown) {
  return [
    ...(Array.isArray(value) ? value : []),
  ]
    .map((item) => normalizeText(item))
    .filter(Boolean)
    .filter((item, index, array) => array.indexOf(item) === index);
}

function normalizePromptAttachment(value: unknown): ChatComposerAttachment | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }

  const record = value as Record<string, unknown>;
  const id = normalizeText(record.id);
  const name = normalizeText(record.name);
  const path = normalizeText(record.path);
  const publicUrl = normalizeText(record.publicUrl);
  const size = Number(record.size);
  const mimeType = normalizeText(record.mimeType);

  if (!id || !name || !path || !publicUrl || !Number.isFinite(size) || size < 0 || !mimeType) {
    return null;
  }

  return {
    id,
    name,
    path,
    publicUrl,
    size,
    mimeType,
  };
}

function normalizePromptAttachments(value: unknown) {
  if (!Array.isArray(value)) {
    return [] as ChatComposerAttachment[];
  }

  const seen = new Set<string>();

  return value
    .map((item) => normalizePromptAttachment(item))
    .filter((item): item is ChatComposerAttachment => Boolean(item))
    .filter((item) => {
      if (seen.has(item.id)) {
        return false;
      }

      seen.add(item.id);
      return true;
    });
}

function normalizePromptSteps(value: unknown): PromptStep[] {
  if (!Array.isArray(value)) {
    return [];
  }

  return value.flatMap((item, index) => {
    if (!item || typeof item !== 'object' || Array.isArray(item)) {
      return [];
    }

    const record = item as Record<string, unknown>;
    const key = normalizeText(record.key) || `step-${index + 1}`;
    const title = normalizeText(record.title);
    const options = Array.isArray(record.options)
      ? record.options
        .map((option) => normalizePromptOption(option))
        .filter((option): option is PromptOption => Boolean(option))
      : [];

    if (!title || options.length === 0) {
      return [];
    }

    return [
      {
        key,
        title,
        description: normalizeText(record.description) || null,
        submitLabel: normalizeText(record.submitLabel) || null,
        mode: record.mode === 'direct' || record.mode === 'queue' ? record.mode : null,
        multiple: record.multiple === true,
        optional: record.optional === true,
        responseTemplate: normalizeText(record.responseTemplate) || null,
        freeTextLabel: normalizeText(record.freeTextLabel) || null,
        freeTextPlaceholder: normalizeText(record.freeTextPlaceholder) || null,
        selectedValues: normalizePromptSelectedValues(record.selectedValues),
        options,
      },
    ];
  });
}

function decodeUrlComponentSafely(value: string) {
  try {
    return decodeURIComponent(value);
  } catch {
    return value;
  }
}

function resolveLinkCardUrlAndActionLabel(rawUrl: string, rawActionLabel?: string) {
  let resolvedUrl = normalizeText(rawUrl);
  let resolvedActionLabel = normalizeText(rawActionLabel);

  if (!resolvedActionLabel) {
    const decodedUrl = decodeUrlComponentSafely(resolvedUrl);
    const dividerIndex = decodedUrl.lastIndexOf('|');

    if (dividerIndex > 0 && dividerIndex < decodedUrl.length - 1) {
      resolvedUrl = decodedUrl.slice(0, dividerIndex).trim();
      resolvedActionLabel = decodedUrl.slice(dividerIndex + 1).trim();
    }
  }

  return {
    url: normalizeUrl(resolvedUrl),
    actionLabel: resolvedActionLabel || null,
  };
}

function hasKnownFileExtension(url: string) {
  const pathname = url.split('?')[0] ?? '';
  return /\.[a-z0-9]{1,8}$/i.test(pathname);
}

function isInternalResourceUrl(url: string) {
  return RESOURCE_PATH_PREFIXES.some((prefix) => url.startsWith(prefix));
}

function isStructuredLinkCardCandidate(url: string) {
  const normalized = normalizeUrl(url);

  if (!normalized) {
    return false;
  }

  if (isInternalResourceUrl(normalized)) {
    return false;
  }

  return /^https?:\/\//i.test(normalized) && !hasKnownFileExtension(normalized);
}

function buildFallbackLinkTitle(url: string) {
  try {
    const parsed = new URL(url, 'https://local.invalid');
    const lastSegment = parsed.pathname.split('/').filter(Boolean).at(-1)?.trim();
    return lastSegment || parsed.hostname || normalizeText(url);
  } catch {
    return normalizeText(url);
  }
}

function normalizeStandaloneTitle(value: string) {
  return value
    .replace(/^\s*(?:[-*+]\s+|\d+\.\s+)?/, '')
    .replace(/[`'"]+/g, '')
    .replace(/\s+/g, ' ')
    .trim();
}

function resolveStandaloneLinkTitle(keptLines: string[], url: string) {
  for (let index = keptLines.length - 1; index >= 0; index -= 1) {
    const candidate = normalizeStandaloneTitle(keptLines[index] ?? '');

    if (candidate) {
      return candidate;
    }
  }

  return buildFallbackLinkTitle(url);
}

function buildLinkCardPart(rawBody: string): ChatMessagePart | null {
  const segments = rawBody
    .split('|')
    .map((segment) => segment.trim())
    .filter(Boolean);

  if (segments.length < 2) {
    return null;
  }

  const [rawTitle, rawUrl, rawActionLabel] = segments;
  const title = normalizeText(rawTitle);
  const { url, actionLabel } = resolveLinkCardUrlAndActionLabel(rawUrl, rawActionLabel);

  if (!title || !url) {
    return null;
  }

  return {
    type: 'link_card',
    title,
    url,
    actionLabel,
  };
}

function buildPromptPart(rawBody: string): ChatMessagePart | null {
  let parsed: unknown;

  try {
    parsed = JSON.parse(rawBody);
  } catch {
    return null;
  }

  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    return null;
  }

  const record = parsed as Record<string, unknown>;
  const title = normalizeText(record.title);
  const options = Array.isArray(record.options)
    ? record.options
      .map((item) => normalizePromptOption(item))
      .filter((option): option is PromptOption => Boolean(option))
    : [];
  const steps = normalizePromptSteps(record.steps);

  if (!title || (options.length === 0 && steps.length === 0)) {
    return null;
  }

  const mode = record.mode === 'direct' || record.mode === 'queue' ? record.mode : null;
  const selectedValues = [
    ...normalizePromptSelectedValues(record.selectedValues),
    ...(record.selectedValue != null ? [record.selectedValue] : []),
  ]
    .map((item) => normalizeText(item))
    .filter(Boolean)
    .filter((value, index, values) => values.indexOf(value) === index);
  const resolvedBy =
    record.resolvedBy === 'user' || record.resolvedBy === 'timeout' || record.resolvedBy === 'system'
      ? record.resolvedBy
      : null;

  return {
    type: 'prompt',
    title,
    description: normalizeText(record.description) || null,
    submitLabel: normalizeText(record.submitLabel) || null,
    mode,
    multiple: record.multiple === true,
    responseTemplate: normalizeText(record.responseTemplate) || null,
    freeTextLabel: normalizeText(record.freeTextLabel) || null,
    freeTextPlaceholder: normalizeText(record.freeTextPlaceholder) || null,
    currentStepKey: normalizeText(record.currentStepKey) || null,
    steps: steps.length > 0 ? steps : undefined,
    readOnly: record.readOnly === true || resolvedBy != null,
    selectedValues,
    resolvedBy,
    resolvedAt: normalizeText(record.resolvedAt) || null,
    resultText: normalizeText(record.resultText) || null,
    attachments: normalizePromptAttachments(record.attachments),
    options,
  };
}

export function extractChatMessageParts(text: string) {
  const lines = String(text ?? '').split('\n');
  const keptLines: string[] = [];
  const parts: ChatMessagePart[] = [];
  const seenLinkKeys = new Set<string>();
  const pushPart = (nextPart: ChatMessagePart | null) => {
    if (!nextPart) {
      return false;
    }

    const dedupeKey =
      nextPart.type === 'link_card'
        ? `${nextPart.type}:${nextPart.title}:${nextPart.url}:${nextPart.actionLabel ?? ''}`
        : [
            nextPart.type,
            nextPart.title,
            nextPart.options
              .map((option) =>
                [
                  option.value,
                  option.label,
                  option.preview?.type ?? '',
                  option.preview?.url ?? '',
                  option.preview?.content ?? '',
                  option.preview?.title ?? '',
                ].join('|'),
              )
              .join(','),
            (nextPart.steps ?? [])
              .map((step) =>
                [
                  step.key,
                  step.title,
                  step.options.map((option) => `${option.value}:${option.label}`).join(','),
                ].join('|'),
              )
              .join(','),
            nextPart.selectedValues?.join(',') ?? '',
            nextPart.resolvedBy ?? '',
            nextPart.resultText ?? '',
            nextPart.readOnly === true ? 'readonly' : '',
          ].join(':');

    if (seenLinkKeys.has(dedupeKey)) {
      return true;
    }

    seenLinkKeys.add(dedupeKey);
    parts.push(nextPart);
    return true;
  };

  for (const line of lines) {
    const promptMatched = line.match(PROMPT_LINE_PATTERN);

    if (promptMatched) {
      if (!pushPart(buildPromptPart(promptMatched[1] ?? ''))) {
        keptLines.push(line);
      }
      continue;
    }

    const matched = line.match(LINK_CARD_LINE_PATTERN);

    if (!matched) {
      const markdownLinkMatch = line.match(STANDALONE_MARKDOWN_LINK_LINE_PATTERN);
      if (markdownLinkMatch) {
        const [, rawTitle, rawUrl] = markdownLinkMatch;
        if (isStructuredLinkCardCandidate(rawUrl ?? '')) {
          if (pushPart(buildLinkCardPart(`${rawTitle}|${rawUrl}`))) {
            continue;
          }
        }
      }

      const standaloneUrlMatch = line.match(STANDALONE_URL_LINE_PATTERN);
      if (standaloneUrlMatch) {
        const rawUrl = standaloneUrlMatch[1] ?? '';
        if (isStructuredLinkCardCandidate(rawUrl)) {
          if (pushPart(buildLinkCardPart(`${resolveStandaloneLinkTitle(keptLines, rawUrl)}|${rawUrl}`))) {
            continue;
          }
        }
      }

      keptLines.push(line);
      continue;
    }

    if (!pushPart(buildLinkCardPart(matched[1] ?? ''))) {
      keptLines.push(line);
      continue;
    }

    const latestPart = parts.at(-1);
    if (latestPart?.type === 'link_card' && isInternalResourceUrl(latestPart.url)) {
      parts.pop();
      seenLinkKeys.delete(`${latestPart.type}:${latestPart.title}:${latestPart.url}:${latestPart.actionLabel ?? ''}`);
      keptLines.push(latestPart.url);
    }
  }

  return {
    strippedText: keptLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
    parts,
  };
}

export function stringifyChatMessageParts(parts: ChatMessagePart[] | null | undefined) {
  return JSON.stringify(Array.isArray(parts) ? parts : []);
}

export function parseChatMessageParts(value: unknown): ChatMessagePart[] {
  if (Array.isArray(value)) {
    return value
      .map((item) => {
        if (!item || typeof item !== 'object') {
          return null;
        }

        const record = item as Record<string, unknown>;
        if (record.type === 'link_card') {
          const title = normalizeText(record.title);
          const url = normalizeUrl(String(record.url ?? ''));
          const actionLabel = normalizeText(record.actionLabel) || null;

          if (!title || !url) {
            return null;
          }

          return {
            type: 'link_card' as const,
            title,
            url,
            actionLabel,
          };
        }

        if (record.type === 'prompt') {
          const promptPart = buildPromptPart(JSON.stringify(record));
          return promptPart;
        }

        return null;
      })
      .filter(Boolean) as ChatMessagePart[];
  }

  if (typeof value === 'string') {
    try {
      return parseChatMessageParts(JSON.parse(value));
    } catch {
      return [];
    }
  }

  return [];
}
