import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';

import type {
  OneNotePageDto,
  OneNoteResourcePayloadDto,
} from '../onenote/model/dto.js';
import type {
  OneNoteColor,
  OneNoteContentBlock,
  OneNoteImageBlock,
  OneNoteInkBlock,
  OneNoteInkStroke,
  OneNoteLayout,
  OneNoteTextRun,
  OneNoteTextStyle,
} from '../onenote/one/content-model.js';

interface PageReaderProps {
  page?: OneNotePageDto;
  loading: boolean;
  error?: string;
  loadResource?(resourceId: string): Promise<OneNoteResourcePayloadDto>;
  downloadResource?(resourceId: string, filename?: string): Promise<void>;
}

function formatDate(value: string | undefined): string | null {
  if (!value) return null;
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;
  return new Intl.DateTimeFormat(undefined, {
    dateStyle: 'medium',
    timeStyle: 'short',
  }).format(date);
}

export function PageReader({
  page,
  loading,
  error,
  loadResource,
  downloadResource,
}: PageReaderProps) {
  if (loading) {
    return (
      <section className="page-reader page-reader--empty" aria-live="polite">
        <p>Loading structured page content…</p>
      </section>
    );
  }

  if (error) {
    return (
      <section className="page-reader page-reader--error" role="alert">
        <p className="eyebrow">Page unavailable</p>
        <h2>Could not read this page</h2>
        <p>{error}</p>
      </section>
    );
  }

  if (!page) {
    return (
      <section className="page-reader page-reader--empty">
        <p>Select a readable page to inspect its content.</p>
      </section>
    );
  }

  const createdAt = formatDate(page.createdAt);
  const updatedAt = formatDate(page.updatedAt);

  return (
    <article className="page-reader" aria-labelledby="selected-page-title">
      <header className="page-reader__header">
        <p className="eyebrow">Selected page</p>
        <h2 id="selected-page-title">{page.title || 'Untitled page'}</h2>
        {createdAt || updatedAt ? (
          <dl className="page-metadata">
            {createdAt ? (
              <>
                <dt>Created</dt>
                <dd>{createdAt}</dd>
              </>
            ) : null}
            {updatedAt ? (
              <>
                <dt>Modified</dt>
                <dd>{updatedAt}</dd>
              </>
            ) : null}
          </dl>
        ) : null}
      </header>

      <div className="page-content">
        {page.blocks.length === 0 && page.text.length === 0 ? (
          <p className="empty-message">No readable content was extracted.</p>
        ) : page.blocks.length === 0 ? (
          <p className="extracted-text">{page.text}</p>
        ) : (
          <ContentBlocks
            blocks={page.blocks}
            loadResource={loadResource}
            downloadResource={downloadResource}
          />
        )}
      </div>
    </article>
  );
}

function ContentBlocks({
  blocks,
  loadResource,
  downloadResource,
}: {
  blocks: readonly OneNoteContentBlock[];
  loadResource?: PageReaderProps['loadResource'];
  downloadResource?: PageReaderProps['downloadResource'];
}) {
  return blocks.map((block) => (
    <ContentBlockView
      key={block.id}
      block={block}
      loadResource={loadResource}
      downloadResource={downloadResource}
    />
  ));
}

function ContentBlockView({
  block,
  loadResource,
  downloadResource,
}: {
  block: OneNoteContentBlock;
  loadResource?: PageReaderProps['loadResource'];
  downloadResource?: PageReaderProps['downloadResource'];
}) {
  switch (block.kind) {
    case 'text':
      return (
        <p
          className="onenote-text"
          dir={block.rightToLeft ? 'rtl' : undefined}
          style={{
            ...layoutStyle(block.layout),
            ...paragraphStyle(block),
          }}
        >
          {block.runs.map((run, index) => (
            <TextRunView key={`${run.start}-${run.end}-${index}`} run={run} />
          ))}
        </p>
      );
    case 'outline':
      return (
        <section className="onenote-outline" style={layoutStyle(block.layout)}>
          <ContentBlocks
            blocks={block.children}
            loadResource={loadResource}
            downloadResource={downloadResource}
          />
        </section>
      );
    case 'outline-group':
      return (
        <div className="onenote-outline-group">
          <ContentBlocks
            blocks={block.children}
            loadResource={loadResource}
            downloadResource={downloadResource}
          />
        </div>
      );
    case 'outline-element':
      return (
        <div
          className="onenote-outline-element"
          style={{
            marginInlineStart: `${Math.min(block.childLevel ?? 0, 12) * 1.1}rem`,
          }}
        >
          <ContentBlocks
            blocks={block.contents}
            loadResource={loadResource}
            downloadResource={downloadResource}
          />
          <ContentBlocks
            blocks={block.children}
            loadResource={loadResource}
            downloadResource={downloadResource}
          />
        </div>
      );
    case 'table':
      return (
        <div className="onenote-table-scroll" style={layoutStyle(block.layout)}>
          <table
            className={
              block.bordersVisible
                ? 'onenote-table'
                : 'onenote-table onenote-table--borderless'
            }
          >
            {block.columnWidths.length > 0 ? (
              <colgroup>
                {block.columnWidths.map((width, index) => (
                  <col key={index} style={{ width: halfInches(width) }} />
                ))}
              </colgroup>
            ) : null}
            <tbody>
              {block.rows.map((row) => (
                <tr key={row.id}>
                  {row.cells.map((cell) => (
                    <td
                      key={cell.id}
                      style={{
                        backgroundColor: colorCss(cell.backgroundColor),
                        maxWidth: halfInches(cell.maxWidth),
                      }}
                    >
                      <ContentBlocks
                        blocks={cell.blocks}
                        loadResource={loadResource}
                        downloadResource={downloadResource}
                      />
                    </td>
                  ))}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      );
    case 'image':
      return (
        <ImageView
          image={block}
          loadResource={loadResource}
          downloadResource={downloadResource}
        />
      );
    case 'attachment':
      return (
        <AttachmentView
          resourceId={block.resourceId}
          filename={block.filename}
          size={block.size}
          downloadResource={downloadResource}
        />
      );
    case 'ink':
      return <InkView ink={block} />;
    case 'unsupported':
      return (
        <aside className="unsupported-block">
          <strong>Unsupported object 0x{block.sourceJcid.toString(16)}</strong>
          <span>{block.description}</span>
        </aside>
      );
  }
}

function TextRunView({ run }: { run: OneNoteTextRun }) {
  if (run.style.hidden) return null;
  const content = textWithBreaks(run.text);
  const style = textStyle(run.style);
  return run.href ? (
    <a
      className="onenote-link"
      href={run.href}
      target="_blank"
      rel="noopener noreferrer"
      title={run.href}
      style={style}
    >
      {content}
    </a>
  ) : (
    <span style={style}>{content}</span>
  );
}

function textWithBreaks(text: string): ReactNode[] {
  return text
    .replaceAll('\u000b', '\n')
    .split('\n')
    .flatMap((part, index, values) =>
      index + 1 < values.length
        ? [
            <span key={`${index}-text`}>{part}</span>,
            <br key={`${index}-br`} />,
          ]
        : [<span key={`${index}-text`}>{part}</span>]
    );
}

function ImageView({
  image,
  loadResource,
  downloadResource,
}: {
  image: OneNoteImageBlock;
  loadResource?: PageReaderProps['loadResource'];
  downloadResource?: PageReaderProps['downloadResource'];
}) {
  const [state, setState] = useState<
    | { phase: 'loading' }
    | { phase: 'ready'; resourceId: string; url: string }
    | { phase: 'unavailable'; resourceId: string; message: string }
  >({ phase: 'loading' });

  useEffect(() => {
    let disposed = false;
    let objectUrl: string | undefined;
    if (!image.resourceId || !loadResource) {
      return;
    }
    const resourceId = image.resourceId;
    void loadResource(image.resourceId)
      .then((resource) => {
        if (
          disposed ||
          !resource.browserRenderable ||
          (resource.mediaType !== 'image/png' &&
            resource.mediaType !== 'image/jpeg')
        ) {
          if (!disposed) {
            setState({
              phase: 'unavailable',
              resourceId,
              message:
                'This image format can be downloaded but not displayed safely.',
            });
          }
          return;
        }
        objectUrl = URL.createObjectURL(
          new Blob([resource.bytes], { type: resource.mediaType })
        );
        setState({ phase: 'ready', resourceId, url: objectUrl });
      })
      .catch((error: unknown) => {
        if (!disposed) {
          setState({
            phase: 'unavailable',
            resourceId,
            message:
              error instanceof Error
                ? error.message
                : 'The image could not be loaded.',
          });
        }
      });
    return () => {
      disposed = true;
      if (objectUrl) URL.revokeObjectURL(objectUrl);
    };
  }, [image.resourceId, loadResource]);

  const visibleState =
    !image.resourceId || !loadResource
      ? {
          phase: 'unavailable' as const,
          resourceId: image.resourceId ?? '',
          message: 'The image payload is not available.',
        }
      : state.phase !== 'loading' && state.resourceId === image.resourceId
        ? state
        : { phase: 'loading' as const };

  const fallback =
    visibleState.phase === 'unavailable'
      ? visibleState.message
      : 'Loading embedded image…';
  const visual =
    visibleState.phase === 'ready' ? (
      <img
        src={visibleState.url}
        alt={image.altText ?? image.filename ?? ''}
        style={{
          width: halfInches(image.pictureWidth),
          height: halfInches(image.pictureHeight),
        }}
      />
    ) : (
      <div className="image-placeholder" role="img" aria-label={image.altText}>
        {fallback}
      </div>
    );

  return (
    <figure className="onenote-image" style={layoutStyle(image.layout)}>
      {image.href && visibleState.phase === 'ready' ? (
        <a
          href={image.href}
          target="_blank"
          rel="noopener noreferrer"
          title={image.href}
        >
          {visual}
        </a>
      ) : (
        visual
      )}
      {image.altText || image.filename ? (
        <figcaption>{image.altText ?? image.filename}</figcaption>
      ) : null}
      {visibleState.phase === 'unavailable' &&
      image.resourceId &&
      downloadResource ? (
        <button
          type="button"
          onClick={() =>
            void downloadResource(image.resourceId!, image.filename)
          }
        >
          Download original image
        </button>
      ) : null}
    </figure>
  );
}

function InkView({ ink }: { ink: OneNoteInkBlock }) {
  const box = ink.boundingBox ?? boundingBoxFromInk(ink.strokes);
  const padding = Math.max(
    140,
    ...ink.strokes.map((stroke) => stroke.width ?? 0)
  );
  const viewBox = box
    ? `${box.x - padding / 2} ${box.y - padding / 2} ${Math.max(box.width + padding, 1)} ${Math.max(box.height + padding, 1)}`
    : '0 0 1 1';
  const width = box ? Math.max((box.width + padding) / (2540 / 96), 1) : 1;
  const height = box ? Math.max((box.height + padding) / (2540 / 96), 1) : 1;

  return (
    <div className="onenote-ink" style={layoutStyle(ink.layout)}>
      {ink.strokes.length > 0 ? (
        <svg
          viewBox={viewBox}
          width={width}
          height={height}
          role="img"
          aria-label={`${ink.strokes.length} handwritten or drawn stroke${ink.strokes.length === 1 ? '' : 's'}`}
        >
          {ink.strokes.map((stroke, index) => (
            <path
              key={index}
              d={inkPath(stroke)}
              fill="none"
              stroke={inkColor(stroke.color)}
              strokeWidth={Math.max(stroke.width ?? 140, 1)}
              strokeOpacity={
                stroke.transparency === undefined
                  ? undefined
                  : Math.max(0, Math.min(1, (255 - stroke.transparency) / 256))
              }
              strokeLinecap={stroke.penTip === 0 ? 'round' : 'square'}
              strokeLinejoin={stroke.penTip === 0 ? 'round' : 'bevel'}
            />
          ))}
        </svg>
      ) : null}
      {ink.children.map((child) => (
        <InkView key={child.id} ink={child} />
      ))}
    </div>
  );
}

function inkPath(stroke: OneNoteInkStroke): string {
  const [first, ...points] = stroke.points;
  if (!first) return '';
  if (points.length === 0) return `M ${first.x} ${first.y} l 0 0`;
  return `M ${first.x} ${first.y} L ${points
    .map((point) => `${point.x} ${point.y}`)
    .join(' ')}`;
}

function boundingBoxFromInk(
  strokes: readonly OneNoteInkStroke[]
): { x: number; y: number; width: number; height: number } | undefined {
  let xMin = Infinity;
  let yMin = Infinity;
  let xMax = -Infinity;
  let yMax = -Infinity;
  for (const stroke of strokes) {
    for (const point of stroke.points) {
      xMin = Math.min(xMin, point.x);
      yMin = Math.min(yMin, point.y);
      xMax = Math.max(xMax, point.x);
      yMax = Math.max(yMax, point.y);
    }
  }
  return Number.isFinite(xMin)
    ? { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin }
    : undefined;
}

function inkColor(value: number | undefined): string {
  if (value === undefined) return 'currentColor';
  return `rgb(${value & 0xff}, ${(value >>> 8) & 0xff}, ${(value >>> 16) & 0xff})`;
}

function AttachmentView({
  resourceId,
  filename,
  size,
  downloadResource,
}: {
  resourceId?: string;
  filename: string;
  size?: number;
  downloadResource?: PageReaderProps['downloadResource'];
}) {
  const [status, setStatus] = useState<string>();
  const download = async () => {
    if (!resourceId || !downloadResource) return;
    setStatus('Preparing local download…');
    try {
      await downloadResource(resourceId, filename);
      setStatus(undefined);
    } catch (error) {
      setStatus(
        error instanceof Error
          ? error.message
          : 'The attachment could not be downloaded.'
      );
    }
  };
  return (
    <aside className="attachment-card">
      <div>
        <strong>{filename}</strong>
        {size === undefined ? null : <span>{formatBytes(size)}</span>}
      </div>
      <button
        type="button"
        disabled={!resourceId || !downloadResource}
        onClick={() => void download()}
      >
        Download attachment
      </button>
      {status ? <small role="status">{status}</small> : null}
    </aside>
  );
}

function paragraphStyle(
  block: Extract<OneNoteContentBlock, { kind: 'text' }>
): CSSProperties {
  return {
    textAlign:
      block.paragraphAlignment === 'unknown'
        ? undefined
        : block.paragraphAlignment,
    marginTop: halfInches(block.paragraphSpaceBefore),
    marginBottom: halfInches(block.paragraphSpaceAfter),
    lineHeight:
      block.paragraphLineSpacingExact === undefined
        ? undefined
        : halfInches(block.paragraphLineSpacingExact),
  };
}

function textStyle(style: OneNoteTextStyle): CSSProperties {
  const decorations = [
    style.underline ? 'underline' : undefined,
    style.strikethrough ? 'line-through' : undefined,
  ].filter(Boolean);
  return {
    fontFamily: style.font,
    fontSize:
      style.fontSize === undefined
        ? undefined
        : `${Math.min(Math.max(style.fontSize / 2, 6), 144)}pt`,
    fontWeight: style.bold ? 700 : undefined,
    fontStyle: style.italic ? 'italic' : undefined,
    textDecoration: decorations.length > 0 ? decorations.join(' ') : undefined,
    verticalAlign: style.superscript
      ? 'super'
      : style.subscript
        ? 'sub'
        : undefined,
    color: colorCss(style.fontColor),
    backgroundColor: colorCss(style.highlight),
  };
}

function colorCss(color: OneNoteColor | undefined): string | undefined {
  if (!color || color.kind === 'auto') return undefined;
  const red = color.red ?? 0;
  const green = color.green ?? 0;
  const blue = color.blue ?? 0;
  return color.kind === 'rgba'
    ? `rgba(${red}, ${green}, ${blue}, ${(color.alpha ?? 255) / 255})`
    : `rgb(${red}, ${green}, ${blue})`;
}

function layoutStyle(layout: OneNoteLayout): CSSProperties {
  return {
    maxWidth: halfInches(layout.maxWidth),
    maxHeight: halfInches(layout.maxHeight),
    marginInlineStart: halfInches(layout.offsetHorizontal, -1_000, 10_000),
    marginTop: halfInches(layout.offsetVertical, -1_000, 10_000),
  };
}

function halfInches(
  value: number | undefined,
  minimum = 0,
  maximum = 10_000
): string | undefined {
  if (value === undefined || !Number.isFinite(value)) return undefined;
  return `${Math.min(Math.max(value * 48, minimum), maximum)}px`;
}

function formatBytes(size: number): string {
  if (size < 1024) return `${size} B`;
  if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KiB`;
  return `${(size / (1024 * 1024)).toFixed(1)} MiB`;
}
