from __future__ import annotations

import json
from pathlib import Path

from PIL import Image

DEFAULT_STALE_DIALOG_SELECTORS = [
    '[data-tb-test-id="FileDownload-Dlg-Dialog-Glass-Root"]',
    '[data-tb-test-id="FileDownload-Dlg-Dialog-Floater-Root"]',
    '[data-tb-test-id="detailedErrorDialog-Dialog-Glass-Root"]',
    '[data-tb-test-id="detailedErrorDialog-Dialog-Glass"]',
]


def build_export_image_js(
    inner_frame_fragment: str,
    output_path: str,
    *,
    viewport: dict[str, int] | None = None,
    tableau_wait_ms: int = 90000,
    download_wait_ms: int = 60000,
    dialog_selectors: list[str] | None = None,
) -> str:
    """Build Playwright code that exports the currently filtered Tableau view as an image."""
    payload = json.dumps(
        {
            "inner_frame_fragment": inner_frame_fragment,
            "output_path": output_path,
            "viewport": viewport or {},
            "tableau_wait_ms": tableau_wait_ms,
            "download_wait_ms": download_wait_ms,
            "dialog_selectors": dialog_selectors or DEFAULT_STALE_DIALOG_SELECTORS,
        },
        ensure_ascii=False,
    )
    return f"""async function(page) {{
  const spec = {payload};
  if (spec.viewport && spec.viewport.width && spec.viewport.height) {{
    await page.setViewportSize({{
      width: Number(spec.viewport.width),
      height: Number(spec.viewport.height),
    }});
  }}
  await page.waitForLoadState('domcontentloaded').catch(() => null);

  async function inspectFrame(frame, preferredFragment) {{
    const frameUrl = typeof frame.url === 'function' ? frame.url() : '';
    try {{
      const snapshot = await frame.evaluate(() => {{
        const vizCount = window.tableau?.VizManager?.getVizs?.().length || 0;
        return {{
          url: location.href,
          title: document.title,
          readyState: document.readyState,
          hasTableau: !!(window.tableau && window.tableau.VizManager),
          vizCount,
        }};
      }});
      return {{
        ...snapshot,
        frameUrl,
        matchesTargetFrame: !!(preferredFragment && frameUrl && frameUrl.includes(preferredFragment)),
      }};
    }} catch (error) {{
      return {{
        url: frameUrl,
        title: null,
        readyState: null,
        hasTableau: false,
        vizCount: 0,
        frameUrl,
        matchesTargetFrame: !!(preferredFragment && frameUrl && frameUrl.includes(preferredFragment)),
        error: String(error && error.message || error),
      }};
    }}
  }}

  async function locateVizFrame(timeoutMs, preferredFragment) {{
    const deadline = Date.now() + timeoutMs;
    let lastSnapshots = [];
    while (Date.now() < deadline) {{
      const rankedFrames = page.frames()
        .map((frame) => {{
          const frameUrl = typeof frame.url === 'function' ? frame.url() : '';
          const isPreferred = !!(preferredFragment && frameUrl.includes(preferredFragment));
          return {{
            frame,
            rank: isPreferred ? 0 : (frame === page.mainFrame() ? 1 : 2),
          }};
        }})
        .sort((left, right) => left.rank - right.rank);

      lastSnapshots = [];
      for (const item of rankedFrames) {{
        const snapshot = await inspectFrame(item.frame, preferredFragment);
        lastSnapshots.push(snapshot);
        if (snapshot.vizCount > 0) {{
          return {{ frame: item.frame, snapshots: lastSnapshots }};
        }}
      }}

      const stillOnSignin =
        (page.url() || '').includes('/#/signin') ||
        lastSnapshots.some((snapshot) => (snapshot.url || '').includes('/#/signin'));
      if (stillOnSignin) {{
        throw new Error('Tableau page is still on sign-in: ' + JSON.stringify({{ pageUrl: page.url(), frames: lastSnapshots }}));
      }}

      const hasChromeError =
        (page.url() || '').startsWith('chrome-error://') ||
        lastSnapshots.some((snapshot) => (snapshot.url || '').startsWith('chrome-error://'));
      if (hasChromeError) {{
        throw new Error('Tableau page failed to load: ' + JSON.stringify({{ pageUrl: page.url(), frames: lastSnapshots }}));
      }}

      await page.waitForTimeout(1000);
    }}

    throw new Error('Timed out waiting for Tableau viz: ' + JSON.stringify({{ pageUrl: page.url(), frames: lastSnapshots }}));
  }}

  const located = await locateVizFrame(Number(spec.tableau_wait_ms || 90000), spec.inner_frame_fragment || '');
  const targetFrame = located.frame;
  await targetFrame.evaluate((selectors) => {{
    for (const selector of selectors || []) {{
      document.querySelector(selector)?.remove();
    }}
  }}, spec.dialog_selectors).catch(() => null);

  const exportResult = await targetFrame.evaluate(async (config) => {{
    const viz = window.tableau?.VizManager?.getVizs?.()?.[0];
    if (!viz) {{
      throw new Error('Tableau viz is not ready for export.');
    }}
    if (typeof viz.showExportImageDialog !== 'function') {{
      throw new Error('Tableau export image dialog is not available.');
    }}

    const workbook = typeof viz.getWorkbook === 'function' ? viz.getWorkbook() : null;
    const activeSheet = workbook && typeof workbook.getActiveSheet === 'function'
      ? workbook.getActiveSheet()
      : null;

    for (const selector of config.dialog_selectors || []) {{
      document.querySelector(selector)?.remove();
    }}
    viz.showExportImageDialog();
    return {{
      frameUrl: location.href,
      activeSheet: activeSheet && typeof activeSheet.getName === 'function'
        ? activeSheet.getName()
        : null,
    }};
  }}, {{
    dialog_selectors: spec.dialog_selectors,
  }});

  const downloadScopes = [
    page,
    targetFrame,
    ...page.frames().filter((frame) => frame !== targetFrame),
  ];
  const linkCandidates = [
    {{
      selector: '[role="dialog"] a.tabDownloadFileButton, [role="dialog"] button.tabDownloadFileButton',
      text: null,
    }},
    {{
      selector: '[role="dialog"] button, [role="dialog"] a',
      text: /^(下载|Download)$/i,
    }},
    {{
      selector: 'a.tabDownloadFileButton, button.tabDownloadFileButton',
      text: null,
    }},
  ];
  let link = null;
  for (const scope of downloadScopes) {{
    for (const candidate of linkCandidates) {{
      const locator =
        candidate.text
          ? scope.locator(candidate.selector).filter({{ hasText: candidate.text }}).first()
          : scope.locator(candidate.selector).first();
      try {{
        await locator.waitFor({{ state: 'visible', timeout: 5000 }});
        link = locator;
        break;
      }} catch (error) {{}}
    }}
    if (link) {{
      break;
    }}
  }}
  if (!link) {{
    throw new Error('Export download button was not found after opening the Tableau image export dialog.');
  }}
  const [download] = await Promise.all([
    page.waitForEvent('download', {{ timeout: Number(spec.download_wait_ms || 60000) }}),
    link.click({{ force: true }}),
  ]);
  await download.saveAs(spec.output_path);
  return {{
    ...exportResult,
    pageUrl: page.url(),
    outputPath: spec.output_path,
    suggestedFilename: download.suggestedFilename(),
    frameSnapshots: located.snapshots,
  }};
}}
"""


def normalize_image_size(path: Path, size: dict[str, int] | tuple[int, int]) -> None:
    """Resize an exported Tableau image to the expected crop canvas when needed."""
    if isinstance(size, dict):
        target_size = (int(size["width"]), int(size["height"]))
    else:
        target_size = (int(size[0]), int(size[1]))

    with Image.open(path) as image:
        if image.size == target_size:
            return
        resized = image.resize(target_size, Image.Resampling.LANCZOS)
        resized.save(path)
