import { z } from 'zod'
import { getStroke } from 'perfect-freehand'

/**
 * Pure, framework-agnostic helpers for the project-notes pen canvas (KAR-344).
 *
 * The canvas persists as `project_notes.canvas_data` (jsonb). A canvas is a
 * list of strokes; each stroke is a list of input points `[x, y, pressure]`
 * captured from Pointer-Events (Apple Pencil pressure included). Rendering to
 * an SVG outline is delegated to perfect-freehand so the same geometry is used
 * live in the editor and later for PDF export (Phase 6).
 */

export const CANVAS_VERSION = 1

/** A single captured input point: `[x, y, pressure?]`. */
export type NotePoint = number[]
/** A stroke plus its optional ink color (hex) and pen size. */
export type NoteStroke = { points: NotePoint[]; color?: string; size?: number }
/** A text label placed at `[x, y]` in A4 px coordinates. */
export type NoteText = { x: number; y: number; text: string; color?: string; size?: number }
export type NoteCanvasData = { version: number; strokes: NoteStroke[]; texts?: NoteText[] }

export type StrokeOptions = {
  size?: number
  thinning?: number
  smoothing?: number
  streamline?: number
  simulatePressure?: boolean
}

/** perfect-freehand options shared by live rendering and export. */
export const DEFAULT_STROKE_OPTIONS: Required<StrokeOptions> = {
  size: 6,
  thinning: 0.6,
  smoothing: 0.5,
  streamline: 0.5,
  // Points carry real Apple-Pencil pressure, so don't fake it.
  simulatePressure: false,
}

/**
 * Pen ink presets. Hex values (stored as SVG `fill`, not Tailwind classes).
 * The brand's reserved logo blue is intentionally excluded (logo-only color).
 */
export const INK_COLORS: ReadonlyArray<{ label: string; value: string }> = [
  { label: 'Petrol', value: '#037493' },
  { label: 'Schwarz', value: '#353A41' },
  { label: 'Rot', value: '#D93025' },
  { label: 'Grün', value: '#2E7D32' },
  { label: 'Blau', value: '#1565C0' },
]

export const STROKE_SIZES: ReadonlyArray<{ label: string; value: number }> = [
  { label: 'Dünn', value: 3 },
  { label: 'Mittel', value: 6 },
  { label: 'Dick', value: 12 },
]

export const DEFAULT_INK = INK_COLORS[0].value
export const DEFAULT_SIZE = DEFAULT_STROKE_OPTIONS.size
/** Default text font size in A4 px (≈ 7.4 pt at 96 dpi). */
export const DEFAULT_TEXT_SIZE = 28

const pointSchema = z.array(z.number()).min(2)
const strokeSchema = z.object({
  points: z.array(pointSchema),
  color: z.string().max(32).optional(),
  size: z.number().positive().max(128).optional(),
})
const textSchema = z.object({
  x: z.number(),
  y: z.number(),
  text: z.string().max(2000),
  color: z.string().max(32).optional(),
  size: z.number().positive().max(400).optional(),
})
const canvasSchema = z.object({
  version: z.number(),
  strokes: z.array(strokeSchema),
  texts: z.array(textSchema).optional().default([]),
})

export function emptyCanvas(): NoteCanvasData {
  return { version: CANVAS_VERSION, strokes: [], texts: [] }
}

/**
 * Coerce arbitrary persisted/serialized input into a valid canvas. Accepts a
 * parsed object or a JSON string. Anything malformed degrades to an empty
 * canvas rather than throwing — a corrupt blob must never break the editor.
 */
export function parseCanvasData(value: unknown): NoteCanvasData {
  if (value == null) return emptyCanvas()

  let candidate = value
  if (typeof candidate === 'string') {
    try {
      candidate = JSON.parse(candidate)
    } catch {
      return emptyCanvas()
    }
  }

  const result = canvasSchema.safeParse(candidate)
  return result.success ? result.data : emptyCanvas()
}

export function isCanvasEmpty(data: NoteCanvasData): boolean {
  const noStrokes = data.strokes.every((s) => s.points.length === 0)
  const noTexts = (data.texts ?? []).every((t) => t.text.trim().length === 0)
  return noStrokes && noTexts
}

export function countPoints(data: NoteCanvasData): number {
  return data.strokes.reduce((sum, s) => sum + s.points.length, 0)
}

/** Convert a perfect-freehand outline into an SVG path `d` attribute. */
function outlineToPath(outline: number[][]): string {
  if (outline.length === 0) return ''
  const d = outline.reduce(
    (acc: (string | number)[], [x0, y0], i, arr) => {
      const [x1, y1] = arr[(i + 1) % arr.length]
      acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
      return acc
    },
    ['M', ...outline[0], 'Q'],
  )
  d.push('Z')
  return d.join(' ')
}

/** Render one captured stroke to an SVG path `d` string. */
export function strokeToSvgPath(points: NotePoint[], options: StrokeOptions = {}): string {
  if (points.length === 0) return ''
  const outline = getStroke(points, { ...DEFAULT_STROKE_OPTIONS, ...options }) as number[][]
  return outlineToPath(outline)
}

export type SvgRenderOptions = {
  width: number
  height: number
  options?: StrokeOptions
}

/**
 * Render a full canvas to a standalone SVG string. The ink uses `currentColor`
 * so the embedding context (editor / PDF) controls the color via CSS.
 */
/** Stroke `<path>` markup only (no `<svg>` wrapper) — for composing pages. */
export function canvasInnerSvg(data: NoteCanvasData, options: StrokeOptions = {}): string {
  return data.strokes
    .map((s) => {
      const d = strokeToSvgPath(s.points, { ...options, ...(s.size ? { size: s.size } : {}) })
      if (!d) return ''
      return `<path d="${d}" fill="${s.color ?? 'currentColor'}" />`
    })
    .filter((p) => p.length > 0)
    .join('')
}

export function canvasToSvg(data: NoteCanvasData, { width, height, options }: SvgRenderOptions): string {
  return (
    `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" ` +
    `viewBox="0 0 ${width} ${height}">${canvasInnerSvg(data, options)}</svg>`
  )
}
