export type PlotPoint = { x: number; y: number }
export type PlotSeriesStyle = { color: string; lineWidth: number; dash?: number[] }
export type PlotSeriesBinding = { sourceId: string; xRange: string; yRange: string }
export type PlotSeries = { id: string; label: string; points: PlotPoint[]; style: PlotSeriesStyle; binding?: PlotSeriesBinding }
export type PlotAxis = { label: string; minimum?: number; maximum?: number; scale: 'linear' | 'log' }
export type PlotSnapshot = { id: string; label: string; xAxis: PlotAxis; yAxis: PlotAxis; legend: boolean; series: PlotSeries[]; version: number }

export type PlotApi = {
  snapshot(): PlotSnapshot
  setAxes(input: { x: PlotAxis; y: PlotAxis }): PlotSnapshot
  setLegend(visible: boolean): PlotSnapshot
  setSeries(series: PlotSeries): PlotSeries
  bindSeries(seriesId: string, binding: PlotSeriesBinding): PlotSeries
  updateBoundSeries(seriesId: string, points: PlotPoint[]): PlotSeries
  exportCsv(): string
  exportSvg(width?: number, height?: number): string
}

const cloneAxis = (axis: PlotAxis): PlotAxis => ({ ...axis })
const cloneSeries = (series: PlotSeries): PlotSeries => ({ ...series, points: series.points.map((point) => ({ ...point })), style: { ...series.style, dash: series.style.dash ? [...series.style.dash] : undefined }, binding: series.binding ? { ...series.binding } : undefined })
const xmlEscape = (value: string) => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
const csvEscape = (value: string) => /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value

const validateAxis = (axis: PlotAxis, name: string) => {
  if (axis.minimum !== undefined && !Number.isFinite(axis.minimum)) throw new RangeError(`${name} axis minimum must be finite.`)
  if (axis.maximum !== undefined && !Number.isFinite(axis.maximum)) throw new RangeError(`${name} axis maximum must be finite.`)
  if (axis.minimum !== undefined && axis.maximum !== undefined && axis.minimum >= axis.maximum) throw new RangeError(`${name} axis minimum must be less than maximum.`)
  if (axis.scale === 'log' && ((axis.minimum !== undefined && axis.minimum <= 0) || (axis.maximum !== undefined && axis.maximum <= 0))) throw new RangeError(`${name} log axis bounds must be greater than zero.`)
}

const validateSeries = (series: PlotSeries, xAxis?: PlotAxis, yAxis?: PlotAxis) => {
  if (!series.id.trim() || !series.label.trim()) throw new RangeError('Plot series requires an id and label.')
  if (series.points.length < 2 || series.points.some((point) => !Number.isFinite(point.x) || !Number.isFinite(point.y))) throw new RangeError('Plot series requires at least two finite points.')
  if (!/^#[0-9a-f]{6}$/i.test(series.style.color)) throw new RangeError(`Plot series color must be a six-digit hex value: ${series.style.color}`)
  if (!Number.isFinite(series.style.lineWidth) || series.style.lineWidth <= 0 || series.style.lineWidth > 20) throw new RangeError('Plot series line width must be greater than zero and no more than 20.')
  if (series.style.dash?.some((value) => !Number.isFinite(value) || value <= 0)) throw new RangeError('Plot dash values must be positive finite numbers.')
  if (xAxis?.scale === 'log' && series.points.some((point) => point.x <= 0)) throw new RangeError('Plot log X axis requires strictly positive series values.')
  if (yAxis?.scale === 'log' && series.points.some((point) => point.y <= 0)) throw new RangeError('Plot log Y axis requires strictly positive series values.')
}

const bounds = (series: PlotSeries[], axis: 'x' | 'y', configured: PlotAxis) => {
  const values = series.flatMap((entry) => entry.points.map((point) => point[axis]))
  const minimum = configured.minimum ?? Math.min(...values)
  const maximum = configured.maximum ?? Math.max(...values)
  if (!Number.isFinite(minimum) || !Number.isFinite(maximum)) return { minimum: 0, maximum: 1 }
  if (configured.scale === 'log' && (minimum <= 0 || maximum <= 0)) throw new RangeError(`Plot log ${axis.toUpperCase()} axis requires strictly positive values.`)
  if (minimum === maximum) return configured.scale === 'log' ? { minimum: minimum / 10, maximum: maximum * 10 } : { minimum: minimum - 0.5, maximum: maximum + 0.5 }
  return { minimum, maximum }
}

export const createPlot = (id = 'plot', label = 'Plot'): PlotApi => {
  let xAxis: PlotAxis = { label: 'X', scale: 'linear' }
  let yAxis: PlotAxis = { label: 'Y', scale: 'linear' }
  let legend = true
  let version = 0
  const seriesById = new Map<string, PlotSeries>()
  const snapshot = (): PlotSnapshot => ({ id, label, xAxis: cloneAxis(xAxis), yAxis: cloneAxis(yAxis), legend, series: [...seriesById.values()].map(cloneSeries), version })
  const setAxes = (input: { x: PlotAxis; y: PlotAxis }) => { validateAxis(input.x, 'X'); validateAxis(input.y, 'Y'); for (const series of seriesById.values()) validateSeries(series, input.x, input.y); xAxis = cloneAxis(input.x); yAxis = cloneAxis(input.y); version += 1; return snapshot() }
  const setLegend = (visible: boolean) => { legend = visible; version += 1; return snapshot() }
  const setSeries = (series: PlotSeries) => { validateSeries(series, xAxis, yAxis); const copy = cloneSeries(series); seriesById.set(copy.id, copy); version += 1; return cloneSeries(copy) }
  const bindSeries = (seriesId: string, binding: PlotSeriesBinding) => {
    const series = seriesById.get(seriesId)
    if (!series) throw new RangeError(`Plot series does not exist: ${seriesId}`)
    if (!binding.sourceId || !binding.xRange || !binding.yRange) throw new RangeError('Plot series binding requires source and ranges.')
    series.binding = { ...binding }
    version += 1
    return cloneSeries(series)
  }
  const updateBoundSeries = (seriesId: string, points: PlotPoint[]) => {
    const series = seriesById.get(seriesId)
    if (!series?.binding) throw new RangeError(`Plot series is not bound: ${seriesId}`)
    return setSeries({ ...cloneSeries(series), points })
  }
  const exportCsv = () => {
    const rows = [['series', 'label', 'x', 'y']]
    for (const series of seriesById.values()) for (const point of series.points) rows.push([series.id, series.label, String(point.x), String(point.y)])
    return rows.map((row) => row.map(csvEscape).join(',')).join('\n')
  }
  const exportSvg = (width = 640, height = 360) => {
    if (!Number.isFinite(width) || !Number.isFinite(height) || width < 200 || height < 160 || width > 4096 || height > 4096) throw new RangeError('Plot SVG dimensions are outside the supported range.')
    const series = [...seriesById.values()]
    const x = bounds(series, 'x', xAxis)
    const y = bounds(series, 'y', yAxis)
    const margin = { left: 56, right: legend ? 144 : 24, top: 24, bottom: 48 }
    const plotWidth = width - margin.left - margin.right
    const plotHeight = height - margin.top - margin.bottom
    const normalize = (value: number, axis: PlotAxis, range: { minimum: number; maximum: number }) => axis.scale === 'log'
      ? (Math.log10(value) - Math.log10(range.minimum)) / (Math.log10(range.maximum) - Math.log10(range.minimum))
      : (value - range.minimum) / (range.maximum - range.minimum)
    const mapX = (value: number) => margin.left + normalize(value, xAxis, x) * plotWidth
    const mapY = (value: number) => margin.top + plotHeight - normalize(value, yAxis, y) * plotHeight
    const paths = series.map((entry) => {
      const path = entry.points.map((point, index) => `${index === 0 ? 'M' : 'L'}${mapX(point.x).toFixed(3)} ${mapY(point.y).toFixed(3)}`).join(' ')
      const dash = entry.style.dash ? ` stroke-dasharray="${entry.style.dash.join(' ')}"` : ''
      return `<path data-series="${xmlEscape(entry.id)}" d="${path}" fill="none" stroke="${entry.style.color}" stroke-width="${entry.style.lineWidth}"${dash}/>`
    }).join('')
    const legendItems = legend ? series.map((entry, index) => `<g transform="translate(${width - margin.right + 18} ${margin.top + 18 + index * 22})"><line x2="22" stroke="${entry.style.color}" stroke-width="${entry.style.lineWidth}"/><text x="30" y="4">${xmlEscape(entry.label)}</text></g>`).join('') : ''
    return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="${xmlEscape(label)}" data-x-scale="${xAxis.scale}" data-y-scale="${yAxis.scale}"><rect width="100%" height="100%" fill="#ffffff"/><g font-family="sans-serif" font-size="12" fill="#202428"><line x1="${margin.left}" y1="${margin.top + plotHeight}" x2="${margin.left + plotWidth}" y2="${margin.top + plotHeight}" stroke="#59616a"/><line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + plotHeight}" stroke="#59616a"/>${paths}<text x="${margin.left + plotWidth / 2}" y="${height - 12}" text-anchor="middle">${xmlEscape(xAxis.label)}</text><text transform="translate(16 ${margin.top + plotHeight / 2}) rotate(-90)" text-anchor="middle">${xmlEscape(yAxis.label)}</text>${legendItems}</g></svg>`
  }
  return { snapshot, setAxes, setLegend, setSeries, bindSeries, updateBoundSeries, exportCsv, exportSvg }
}
