import { OOXML, W } from './namespaces.js';
import { getAttributeSafe, getFirstChild } from './xml-helpers.js';

function getWAttr(el: Element, localName: string): string | null {
  // Preserve legacy truthy fallback for empty strings from namespace-bound reads
  // when attributes were written without a real namespace binding.
  return getAttributeSafe(el, OOXML.W_NS, localName, 'w', { emptyIsMissing: true });
}

export type StyleDef = {
  styleId: string;
  name: string;
  basedOn: string | null;
  pPr: Element | null;
  rPr: Element | null;
};

export type StylesModel = {
  byId: Map<string, StyleDef>;
};

export function parseStylesXml(stylesDoc: Document | null): StylesModel {
  const byId = new Map<string, StyleDef>();
  if (!stylesDoc) return { byId };

  const styles = Array.from(stylesDoc.getElementsByTagNameNS(OOXML.W_NS, W.style));
  for (const st of styles) {
    const id = getWAttr(st, 'styleId');
    if (!id) continue;
    const nameEl = getFirstChild(st, OOXML.W_NS, W.name);
    const basedOnEl = getFirstChild(st, OOXML.W_NS, W.basedOn);
    const pPr = getFirstChild(st, OOXML.W_NS, W.pPr);
    const rPr = getFirstChild(st, OOXML.W_NS, W.rPr);

    const name = nameEl ? (getWAttr(nameEl, 'val') ?? id) : id;
    const basedOn = basedOnEl ? (getWAttr(basedOnEl, 'val') ?? null) : null;

    byId.set(id, {
      styleId: id,
      name,
      basedOn,
      pPr: pPr ?? null,
      rPr: rPr ?? null,
    });
  }
  return { byId };
}

function resolveStyleChain(model: StylesModel, styleId: string | null): StyleDef[] {
  const chain: StyleDef[] = [];
  let cur = styleId;
  const seen = new Set<string>();
  while (cur) {
    if (seen.has(cur)) break;
    seen.add(cur);
    const st = model.byId.get(cur);
    if (!st) break;
    chain.push(st);
    cur = st.basedOn;
  }
  return chain;
}

export type ParagraphAlignment = 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFY';

export type ParagraphFormatting = {
  styleId: string | null;
  styleName: string;
  alignment: ParagraphAlignment;
  leftIndentPt: number;
  firstLineIndentPt: number;
};

function twipsToPt(v: number): number {
  return v / 20.0;
}

function parseIndentPt(indEl: Element | null): { leftIndentPt: number; firstLineIndentPt: number } {
  if (!indEl) return { leftIndentPt: 0, firstLineIndentPt: 0 };
  const left = Number.parseInt(getWAttr(indEl, 'left') ?? '0', 10);
  const firstLine = getWAttr(indEl, 'firstLine');
  const hanging = getWAttr(indEl, 'hanging');
  let first = 0;
  if (firstLine != null) first = Number.parseInt(firstLine, 10) || 0;
  else if (hanging != null) first = -(Number.parseInt(hanging, 10) || 0);
  return { leftIndentPt: twipsToPt(left), firstLineIndentPt: twipsToPt(first) };
}

function parseAlignment(jcEl: Element | null): ParagraphAlignment {
  const val = jcEl ? (getWAttr(jcEl, 'val') ?? '') : '';
  switch (val) {
    case 'center':
      return 'CENTER';
    case 'right':
      return 'RIGHT';
    case 'both':
    case 'justify':
      return 'JUSTIFY';
    case 'left':
    default:
      return 'LEFT';
  }
}

function firstNonNull<T>(vals: Array<T | null | undefined>): T | null {
  for (const v of vals) {
    if (v !== null && v !== undefined) return v as T;
  }
  return null;
}

export function extractParagraphFormatting(
  pPr: Element | null,
  styles: StylesModel,
): ParagraphFormatting {
  const pStyleEl = pPr ? getFirstChild(pPr, OOXML.W_NS, W.pStyle) : null;
  const styleId = pStyleEl ? (getWAttr(pStyleEl, 'val') ?? null) : null;

  const chain = resolveStyleChain(styles, styleId);
  const styleName = (styleId && styles.byId.get(styleId)?.name) || styleId || '';

  // Resolve alignment and indents: direct pPr overrides style chain.
  const directJc = pPr ? getFirstChild(pPr, OOXML.W_NS, W.jc) : null;
  const directInd = pPr ? getFirstChild(pPr, OOXML.W_NS, W.ind) : null;

  const styleJc = firstNonNull(chain.map((s) => (s.pPr ? getFirstChild(s.pPr, OOXML.W_NS, W.jc) : null)));
  const styleInd = firstNonNull(chain.map((s) => (s.pPr ? getFirstChild(s.pPr, OOXML.W_NS, W.ind) : null)));

  const alignment = parseAlignment(directJc ?? styleJc);
  const ind = parseIndentPt(directInd ?? styleInd);

  return {
    styleId,
    styleName,
    alignment,
    leftIndentPt: ind.leftIndentPt,
    firstLineIndentPt: ind.firstLineIndentPt,
  };
}

export type RunFormatting = {
  bold: boolean;
  italic: boolean;
  underline: boolean;
  highlightVal: string | null;
  fontName: string;
  fontSizePt: number;
  colorHex: string | null;
};

/**
 * Tri-state run formatting resolved from a named style's `basedOn` chain: `null` means no
 * chain member specifies the property (distinct from an explicit `w:val="0"` → `false`).
 * Consumers seeding their own style templates (e.g. the DOCX → ODT converter's `styles.xml`)
 * use `null` to fall back to template defaults instead of overriding them.
 */
export type StyleRunFormatting = {
  bold: boolean | null;
  italic: boolean | null;
  fontName: string | null;
  fontSizePt: number | null;
  colorHex: string | null;
};

/** Resolve a named style's effective run formatting through its `basedOn` chain. */
export function extractStyleRunFormatting(
  styles: StylesModel,
  styleId: string | null,
): StyleRunFormatting {
  const rPrs = resolveStyleChain(styles, styleId).map((s) => s.rPr);
  return {
    bold: firstNonNull(rPrs.map((rPr) => parseBoolProp(rPr, W.b))),
    italic: firstNonNull(rPrs.map((rPr) => parseBoolProp(rPr, W.i))),
    fontName: firstNonNull(rPrs.map(parseFontName)),
    fontSizePt: firstNonNull(rPrs.map(parseFontSizePt)),
    colorHex: firstNonNull(rPrs.map(parseColorHex)),
  };
}

function parseBoolProp(parent: Element | null, tagLocal: string): boolean | null {
  if (!parent) return null;
  const el = getFirstChild(parent, OOXML.W_NS, tagLocal);
  if (!el) return null;
  // <w:b/> implies true. <w:b w:val="0"/> implies false.
  const v = getWAttr(el, 'val');
  if (v === '0' || v === 'false') return false;
  return true;
}

function parseUnderline(parent: Element | null): boolean | null {
  if (!parent) return null;
  const el = getFirstChild(parent, OOXML.W_NS, W.u);
  if (!el) return null;
  const v = getWAttr(el, 'val');
  if (!v) return true;
  return v !== 'none';
}

function parseFontName(parent: Element | null): string | null {
  if (!parent) return null;
  const el = getFirstChild(parent, OOXML.W_NS, W.rFonts);
  if (!el) return null;
  return getWAttr(el, 'ascii') ?? getWAttr(el, 'hAnsi') ?? getWAttr(el, 'cs') ?? getWAttr(el, 'val') ?? null;
}

function parseFontSizePt(parent: Element | null): number | null {
  if (!parent) return null;
  const el = getFirstChild(parent, OOXML.W_NS, W.sz);
  if (!el) return null;
  const valStr = getWAttr(el, 'val') || el.getAttribute('val');
  if (!valStr) return null;
  const v = Number.parseInt(valStr, 10);
  if (Number.isNaN(v)) return null;
  // OOXML stores half-points.
  return v / 2.0;
}

function parseColorHex(parent: Element | null): string | null {
  if (!parent) return null;
  const el = getFirstChild(parent, OOXML.W_NS, W.color);
  if (!el) return null;
  const v = getWAttr(el, 'val') || el.getAttribute('val');
  if (!v || v === 'auto') return null;
  return v;
}

function parseHighlightVal(parent: Element | null): string | null {
  if (!parent) return null;
  const el = getFirstChild(parent, OOXML.W_NS, W.highlight);
  if (!el) return null;
  const v = getWAttr(el, 'val');
  if (!v || v === 'none') return null;
  return v;
}

export function extractEffectiveRunFormatting(params: {
  run: Element;
  paragraphPPr: Element | null;
  paragraphStyleId: string | null;
  styles: StylesModel;
}): RunFormatting {
  const { run, paragraphPPr, paragraphStyleId, styles } = params;
  const isRun = run.localName === W.r || run.localName === 'r';
  const rPr = isRun ? getFirstChild(run, OOXML.W_NS, W.rPr) : null;
  const pRPr = paragraphPPr ? getFirstChild(paragraphPPr, OOXML.W_NS, W.rPr) : null;

  // Resolve w:rStyle character style chain (e.g. "Strong" → bold via style definition).
  const rStyleEl = rPr ? getFirstChild(rPr, OOXML.W_NS, W.rStyle) : null;
  const rStyleId = rStyleEl ? (getWAttr(rStyleEl, 'val') ?? null) : null;
  const rStyleChain = resolveStyleChain(styles, rStyleId);
  const rStyleRPr = firstNonNull(rStyleChain.map((s) => s.rPr));

  const paraChain = resolveStyleChain(styles, paragraphStyleId);
  const styleRPr = firstNonNull(paraChain.map((s) => s.rPr));

  // Priority: direct rPr → rStyle chain rPr → paragraph rPr → paragraph style chain rPr
  const bold = firstNonNull([parseBoolProp(rPr, W.b), parseBoolProp(rStyleRPr, W.b), parseBoolProp(pRPr, W.b), parseBoolProp(styleRPr, W.b)]) ?? false;
  const italic = firstNonNull([parseBoolProp(rPr, W.i), parseBoolProp(rStyleRPr, W.i), parseBoolProp(pRPr, W.i), parseBoolProp(styleRPr, W.i)]) ?? false;
  const underline = firstNonNull([parseUnderline(rPr), parseUnderline(rStyleRPr), parseUnderline(pRPr), parseUnderline(styleRPr)]) ?? false;
  const highlightVal = firstNonNull([parseHighlightVal(rPr), parseHighlightVal(rStyleRPr), parseHighlightVal(pRPr), parseHighlightVal(styleRPr)]);
  const fontName = firstNonNull([parseFontName(rPr), parseFontName(rStyleRPr), parseFontName(pRPr), parseFontName(styleRPr)]) ?? '';
  const fontSizePt = firstNonNull([parseFontSizePt(rPr), parseFontSizePt(rStyleRPr), parseFontSizePt(pRPr), parseFontSizePt(styleRPr)]) ?? 0;
  const colorHex = firstNonNull([parseColorHex(rPr), parseColorHex(rStyleRPr), parseColorHex(pRPr), parseColorHex(styleRPr)]);

  return {
    bold,
    italic,
    underline,
    highlightVal: highlightVal ?? null,
    fontName,
    fontSizePt,
    colorHex: colorHex ?? null,
  };
}
