import { Box, Text } from "ink";
import { parseMarkdown, type MarkdownNode } from "../utils/markdown";

export interface MarkdownRendererProps {
  content: string;
  /** Available width for the README content, excluding its surrounding box. */
  width?: number;
  maxHeight?: number;
  scrollOffset?: number;
}

type SegmentStyle = "plain" | "heading1" | "heading2" | "heading" | "bold" | "italic" | "code" | "link" | "muted";

interface RenderSegment {
  text: string;
  style: SegmentStyle;
}

interface RenderLine {
  segments: RenderSegment[];
}

function appendSegment(line: RenderLine, segment: RenderSegment): void {
  if (!segment.text) return;
  const previous = line.segments[line.segments.length - 1];
  if (previous?.style === segment.style) {
    previous.text += segment.text;
  } else {
    line.segments.push({ ...segment });
  }
}

function segmentLength(segments: RenderSegment[]): number {
  return segments.reduce((length, segment) => length + segment.text.length, 0);
}

/**
 * Wrap styled text without allowing a long URL, identifier, or path to escape
 * the README viewport. Whitespace is used as a preferred break point, then
 * long unbroken tokens are hard-wrapped.
 */
function wrapSegments(segments: RenderSegment[], rawWidth: number): RenderLine[] {
  const width = Math.max(1, Math.floor(rawWidth));
  const lines: RenderLine[] = [];
  let current: RenderLine = { segments: [] };

  const pushCurrent = () => {
    lines.push(current);
    current = { segments: [] };
  };

  for (const segment of segments) {
    let remaining = segment.text.replace(/\n/g, " ");

    while (remaining.length > 0) {
      const whitespaceMatch = remaining.match(/^\s+/);
      if (whitespaceMatch) {
        const whitespace = whitespaceMatch[0];
        remaining = remaining.slice(whitespace.length);

        // Do not put leading whitespace on a wrapped line.
        if (segmentLength(current.segments) === 0) continue;

        const available = width - segmentLength(current.segments);
        if (available <= 0) {
          pushCurrent();
          continue;
        }

        const kept = whitespace.slice(0, available);
        appendSegment(current, { text: kept, style: segment.style });
        if (kept.length < whitespace.length || segmentLength(current.segments) >= width) {
          pushCurrent();
        }
        continue;
      }

      const nextWhitespace = remaining.search(/\s/);
      const token = nextWhitespace === -1 ? remaining : remaining.slice(0, nextWhitespace);
      remaining = nextWhitespace === -1 ? "" : remaining.slice(nextWhitespace);

      if (segmentLength(current.segments) >= width) pushCurrent();
      const available = width - segmentLength(current.segments);
      if (token.length > available && segmentLength(current.segments) > 0) {
        pushCurrent();
        continue;
      }

      const take = Math.min(token.length, width - segmentLength(current.segments));
      appendSegment(current, { text: token.slice(0, take), style: segment.style });
      if (take < token.length || segmentLength(current.segments) >= width) {
        pushCurrent();
        remaining = token.slice(take) + remaining;
      }
    }
  }

  if (segmentLength(current.segments) > 0 || lines.length === 0) {
    lines.push(current);
  }

  return lines;
}

function inlineSegments(children: MarkdownNode[] | undefined): RenderSegment[] {
  if (!children) return [];

  return children.flatMap((child): RenderSegment[] => {
    const text = child.content ?? "";
    switch (child.type) {
      case "bold":
        return [{ text, style: "bold" }];
      case "italic":
        return [{ text, style: "italic" }];
      case "code":
        return [{ text, style: "code" }];
      case "link":
        return [{ text, style: "link" }];
      default:
        return [{ text, style: "plain" }];
    }
  });
}

function inlineSegmentsFromText(text: string): RenderSegment[] {
  const firstNode = parseMarkdown(text)[0];
  if (firstNode?.type === "paragraph") return inlineSegments(firstNode.children);
  return [{ text, style: "plain" }];
}

function withPrefix(
  content: RenderSegment[],
  prefix: string,
  width: number,
  continuationPrefix = prefix,
): RenderLine[] {
  if (width <= prefix.length) {
    return [{ segments: [{ text: prefix.slice(0, Math.max(1, width)), style: "plain" }] }];
  }

  const prefixWidth = prefix.length;
  const contentLines = wrapSegments(content, Math.max(1, width - prefixWidth));
  return contentLines.map((line, index) => ({
    segments: [
      { text: index === 0 ? prefix : continuationPrefix, style: "plain" },
      ...line.segments,
    ],
  }));
}

function codeBorder(width: number, left: string, right: string): string {
  if (width <= 1) return left.slice(0, width);
  return `${left}${"─".repeat(Math.max(0, width - 2))}${right}`;
}

function renderNode(node: MarkdownNode, width: number, indentLevel = 0): RenderLine[] {
  const indent = "  ".repeat(indentLevel);

  switch (node.type) {
    case "heading": {
      const style: SegmentStyle = node.level === 1 ? "heading1" : node.level === 2 ? "heading2" : "heading";
      return wrapSegments([{ text: node.content ?? "", style }], width);
    }

    case "paragraph":
      return wrapSegments(inlineSegments(node.children), width);

    case "codeblock": {
      const innerWidth = Math.max(1, width - 4);
      const lines: RenderLine[] = [
        { segments: [{ text: codeBorder(width, "┌", "┐"), style: "muted" }] },
      ];
      for (const rawLine of (node.content ?? "").split("\n")) {
        const chunks = rawLine.length === 0
          ? [""]
          : Array.from({ length: Math.ceil(rawLine.length / innerWidth) }, (_, index) =>
              rawLine.slice(index * innerWidth, (index + 1) * innerWidth)
            );
        for (const chunk of chunks) {
          const lineText = `│ ${chunk} │`;
          lines.push({ segments: [{ text: lineText.slice(0, width), style: "plain" }] });
        }
      }
      lines.push({ segments: [{ text: codeBorder(width, "└", "┘"), style: "muted" }] });
      return lines;
    }

    case "list":
      return (node.children ?? []).flatMap((item) =>
        withPrefix(inlineSegmentsFromText(item.content ?? ""), `${indent}• `, width, `${indent}  `)
      );

    case "blockquote":
      return withPrefix(inlineSegmentsFromText(node.content ?? ""), "│ ", width);

    case "hr":
      return [{ segments: [{ text: "─".repeat(Math.max(1, width)), style: "muted" }] }];

    default:
      return [];
  }
}

/** Return the exact physical lines used by MarkdownRenderer at a width. */
export function getMarkdownLines(content: string, width = 80): RenderLine[] {
  return parseMarkdown(content).flatMap((node) => renderNode(node, width));
}

/** Return the physical line count used for README scrolling. */
export function getMarkdownLineCount(content: string, width = 80): number {
  return getMarkdownLines(content, width).length;
}

function renderSegment(segment: RenderSegment, key: string): JSX.Element {
  switch (segment.style) {
    case "heading1":
      return <Text key={key} bold color="cyan">{segment.text}</Text>;
    case "heading2":
      return <Text key={key} bold color="blue">{segment.text}</Text>;
    case "heading":
      return <Text key={key} bold>{segment.text}</Text>;
    case "bold":
      return <Text key={key} bold>{segment.text}</Text>;
    case "italic":
      return <Text key={key} dimColor>{segment.text}</Text>;
    case "code":
      return <Text key={key} inverse>{segment.text}</Text>;
    case "link":
      return <Text key={key} color="cyan" underline>{segment.text}</Text>;
    case "muted":
      return <Text key={key} dimColor>{segment.text}</Text>;
    default:
      return <Text key={key}>{segment.text}</Text>;
  }
}

export function MarkdownRenderer({
  content,
  width = 80,
  maxHeight,
  scrollOffset = 0,
}: MarkdownRendererProps): JSX.Element {
  const lines = getMarkdownLines(content, width);
  const start = Math.max(0, Math.min(scrollOffset, Math.max(0, lines.length - 1)));
  const visibleLines = maxHeight === undefined
    ? lines.slice(start)
    : lines.slice(start, start + Math.max(0, maxHeight));

  return (
    <Box flexDirection="column">
      {visibleLines.map((line, lineIndex) => (
        <Box key={`${start}-${lineIndex}`}>
          <Text>{line.segments.map((segment, segmentIndex) =>
            renderSegment(segment, `${start}-${lineIndex}-${segmentIndex}`)
          )}</Text>
        </Box>
      ))}
    </Box>
  );
}
