const { useCallback, useEffect, useState } = React;
const APP_CONFIG = window.INVOICE_EDITOR_CONFIG ?? {};

// Business identity, bank details, and the Google Sheet reference are
// intentionally NOT hardcoded here. Real personal/financial data must never
// be committed to source control. Configure real values in a local,
// git-ignored invoice-config.js (see invoice-config.example.js for the
// template and README.md for setup instructions). Generic placeholders are
// used when no config is supplied so the app still runs out of the box.
const GOOGLE_SHEET_ID = APP_CONFIG.googleSheetId || "";
const GOOGLE_SHEET_URL = APP_CONFIG.googleSheetUrl || "";
const TAX_RATE = 0.1;
const ISSUER_NAME = APP_CONFIG.issuerName || "請求者名（invoice-config.js で設定）";
const ISSUER_ADDRESS_1 = APP_CONFIG.issuerAddress1 || "";
const ISSUER_ADDRESS_2 = APP_CONFIG.issuerAddress2 || "";
const CLIENT_NAME = APP_CONFIG.clientName || "請求先名（invoice-config.js で設定）";

// clientId scopes which client's data to sync in "proxy" mode. A query-param
// override (?client=...) lets one deployment serve multiple clients without a
// rebuild; invoice-config.js's clientId is the default when no override is
// present. Neither is required — deployments that only ever serve one client
// can leave both unset.
function resolveClientId() {
  const fromQuery = new URLSearchParams(window.location.search).get("client");
  return fromQuery || APP_CONFIG.clientId || "";
}

const CLIENT_ID = resolveClientId();
const BANK_NAME = APP_CONFIG.bankName || "";
const BANK_BRANCH = APP_CONFIG.bankBranch || "";
const BANK_ACCOUNT = APP_CONFIG.bankAccount || "";

const MONTHS = [
  { key: "January", label: "1月" },
  { key: "February", label: "2月" },
  { key: "March", label: "3月" },
  { key: "April", label: "4月" },
  { key: "May", label: "5月" },
  { key: "June", label: "6月" },
  { key: "July", label: "7月" },
  { key: "August", label: "8月" },
  { key: "September", label: "9月" },
  { key: "October", label: "10月" },
  { key: "November", label: "11月" },
  { key: "December", label: "12月" },
];

const FALLBACK_EXPENSE_DATA = {
  January: {},
  February: {},
  March: { "調査業務 (Labor)": 200000 },
  April: {
    "調査業務 (Labor)": 200000,
    Equipment: 69325,
    "Subcontractor (Cleaning)": 6000,
  },
  May: {
    "調査業務 (Labor)": 200000,
    Equipment: 38041.67,
    "Air Purifier": 14000,
    Roomba: 10000,
    "Starlink subscription": 15600,
  },
  June: {
    "調査業務 (Labor)": 200000,
    Equipment: 34925,
    "Air Purifier": 14000,
    "Starlink subscription": 15600,
  },
  July: {
    "調査業務 (Labor)": 200000,
    Equipment: 24600,
    "Starlink subscription": 15600,
  },
  August: {
    "調査業務 (Labor)": 200000,
    Equipment: 10600,
    "Starlink subscription": 15600,
  },
  September: {
    "調査業務 (Labor)": 200000,
    Equipment: 10600,
    "Starlink subscription": 15600,
  },
  October: {
    "調査業務 (Labor)": 200000,
    Equipment: 10600,
    "Starlink subscription": 15600,
  },
  November: {
    "調査業務 (Labor)": 200000,
    Equipment: 10600,
    "Starlink subscription": 15600,
  },
  December: {
    "調査業務 (Labor)": 200000,
    Equipment: 10600,
    "Starlink subscription": 15600,
  },
};

const STATUS_LABELS = {
  idle: "待機中",
  loading: "同期中",
  ok: "同期済み",
  error: "同期失敗",
};

const SYNC_MODE_LABELS = {
  fallback: "Fallback data",
  proxy: "Proxy sync",
  anthropic: "Anthropic MCP sync",
};

const moneyFormatter = new Intl.NumberFormat("ja-JP", {
  style: "currency",
  currency: "JPY",
  minimumFractionDigits: 0,
  maximumFractionDigits: 2,
});

function monthIndexFromKey(monthKey) {
  return MONTHS.findIndex((month) => month.key === monthKey);
}

function monthLabelFromKey(monthKey) {
  return MONTHS.find((month) => month.key === monthKey)?.label ?? monthKey;
}

function formatCurrency(value) {
  return moneyFormatter.format(Number.isFinite(value) ? value : 0);
}

function formatDate(date, includeTime = false) {
  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();

  if (!includeTime) {
    return `${year}年${month}月${day}日`;
  }

  const hours = String(date.getHours()).padStart(2, "0");
  const minutes = String(date.getMinutes()).padStart(2, "0");
  const seconds = String(date.getSeconds()).padStart(2, "0");
  return `${year}年${month}月${day}日 ${hours}:${minutes}:${seconds}`;
}

function formatDueDate(year, monthIndex) {
  const lastDay = new Date(year, monthIndex + 1, 0);
  return formatDate(lastDay, false);
}

function defaultInvoiceYear(monthKey, referenceDate) {
  const monthIndex = monthIndexFromKey(monthKey);

  // There is no explicit year selector by default, only a month dropdown. A
  // selected month that is later in the calendar than "now" almost always
  // means the invoice is for that month in the previous year (e.g. viewing
  // or printing a December invoice in January of the following year).
  // Without this, invoiceNumberForMonth/formatDueDate would silently stamp
  // the wrong year around every year boundary.
  return monthIndex > referenceDate.getMonth() ? referenceDate.getFullYear() - 1 : referenceDate.getFullYear();
}

function invoiceNumberForMonth(year, monthIndex) {
  return `${year}${String(monthIndex + 1).padStart(2, "0")}01-001`;
}

function toNumericString(value) {
  if (!Number.isFinite(value) || value === 0) {
    return value === 0 ? "0" : "";
  }

  const rounded = Number(value.toFixed(2));
  return String(rounded);
}

function createId() {
  return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function toLineItem(description = "", quantity = "1", unitPrice = "", amount = "") {
  return {
    id: createId(),
    description,
    quantity,
    unitPrice,
    amount,
  };
}

function buildLineItemsForMonth(expenseData, monthKey) {
  const monthData = expenseData[monthKey] ?? {};
  const rows = Object.entries(monthData).map(([description, amount]) =>
    toLineItem(description, "1", toNumericString(amount), toNumericString(amount)),
  );

  return rows.length > 0 ? rows : [toLineItem("", "1", "", "")];
}

function monthTotal(expenseData, monthKey) {
  return Object.values(expenseData[monthKey] ?? {}).reduce(
    (sum, amount) => sum + (Number.isFinite(amount) ? amount : 0),
    0,
  );
}

function parseNumericInput(value) {
  const normalized = String(value ?? "").replace(/,/g, "").trim();
  if (!normalized) {
    return NaN;
  }

  return Number.parseFloat(normalized);
}

function normalizeExpenseShape(rawData) {
  const normalized = {};

  MONTHS.forEach(({ key }) => {
    const monthRecord = rawData?.[key];
    const entries = Object.entries(monthRecord ?? {}).reduce((acc, [name, value]) => {
      const numericValue =
        typeof value === "number" ? value : Number.parseFloat(String(value ?? "").replace(/,/g, ""));

      if (Number.isFinite(numericValue) && numericValue > 0) {
        acc[name] = numericValue;
      }

      return acc;
    }, {});

    normalized[key] = entries;
  });

  return normalized;
}

function extractJsonText(responsePayload) {
  const text = (responsePayload?.content ?? [])
    .filter((block) => block?.type === "text")
    .map((block) => block.text)
    .join("\n")
    .replace(/```json/gi, "")
    .replace(/```/g, "")
    .trim();

  if (!text) {
    throw new Error("Anthropic response did not include text content.");
  }

  const firstBrace = text.indexOf("{");
  const lastBrace = text.lastIndexOf("}");

  if (firstBrace !== -1 && lastBrace !== -1) {
    return text.slice(firstBrace, lastBrace + 1);
  }

  return text;
}

function createInvoiceState(monthKey, expenseData, year) {
  const monthIndex = monthIndexFromKey(monthKey);

  return {
    invoiceNumber: invoiceNumberForMonth(year, monthIndex),
    fromName: ISSUER_NAME,
    fromAddress1: ISSUER_ADDRESS_1,
    fromAddress2: ISSUER_ADDRESS_2,
    toName: CLIENT_NAME,
    subject: `調査費用（${monthLabelFromKey(monthKey)}分）`,
    dueDate: formatDueDate(year, monthIndex),
    bankName: BANK_NAME,
    bankBranch: BANK_BRANCH,
    bankAccount: BANK_ACCOUNT,
    lineItems: buildLineItemsForMonth(expenseData, monthKey),
  };
}

function getConfiguredSyncMode() {
  if (APP_CONFIG.syncMode) {
    return APP_CONFIG.syncMode;
  }

  if (APP_CONFIG.syncEndpoint) {
    return "proxy";
  }

  return "fallback";
}

function extractMonthPayload(payload) {
  if (payload && typeof payload === "object" && payload.months && typeof payload.months === "object") {
    return payload.months;
  }

  return payload;
}

const componentStyles = `
  @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Noto+Serif+JP:wght@400;500;600;700&display=swap');

  :root {
    --ink-bg: #1a1209;
    --ink-deep: #24170c;
    --gold: #b5915a;
    --gold-soft: #e2c48a;
    --gold-mid: #c9a96e;
    --paper: #f5f0e8;
    --card: #ffffff;
    --muted: #8e7d67;
    --line: rgba(181, 145, 90, 0.32);
    --banner: #20160d;
    --shadow: 0 22px 58px rgba(0, 0, 0, 0.3);
    --danger: #d36a6a;
    --success: #89c985;
  }

  * {
    box-sizing: border-box;
  }

  html, body, #root {
    min-height: 100%;
  }

  body {
    margin: 0;
    background:
      radial-gradient(circle at top left, rgba(201, 169, 110, 0.16), transparent 28%),
      radial-gradient(circle at bottom right, rgba(226, 196, 138, 0.12), transparent 24%),
      linear-gradient(135deg, #120c06, var(--ink-bg) 38%, #24160a 100%);
  }

  .invoice-editor-shell {
    min-height: 100vh;
    padding: 32px 18px 56px;
    color: var(--paper);
    font-family: "Noto Serif JP", serif;
  }

  .invoice-toolbar {
    max-width: 980px;
    margin: 0 auto 22px;
    padding: 16px 18px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    flex-wrap: wrap;
    border: 1px solid rgba(226, 196, 138, 0.18);
    border-radius: 20px;
    background: rgba(25, 17, 10, 0.74);
    backdrop-filter: blur(14px);
    box-shadow: 0 16px 44px rgba(0, 0, 0, 0.22);
  }

  .toolbar-title {
    margin: 0;
    color: var(--gold-soft);
    font-size: clamp(1.5rem, 3vw, 2rem);
    font-family: "Cormorant Garamond", serif;
    letter-spacing: 0.08em;
  }

  .toolbar-group {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
  }

  .status-pill {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    border-radius: 999px;
    border: 1px solid rgba(226, 196, 138, 0.2);
    background: rgba(255, 255, 255, 0.04);
    color: var(--paper);
    font-size: 0.9rem;
  }

  .status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #9a8d7a;
    box-shadow: 0 0 0 0 rgba(226, 196, 138, 0.55);
  }

  .status-pill.loading .status-dot {
    background: var(--gold-soft);
    animation: pulse-dot 1.1s infinite;
  }

  .status-pill.ok .status-dot {
    background: var(--success);
  }

  .status-pill.error .status-dot {
    background: var(--danger);
  }

  .toolbar-select,
  .toolbar-button,
  .toolbar-link {
    border: 1px solid rgba(226, 196, 138, 0.24);
    background: rgba(255, 255, 255, 0.05);
    color: var(--paper);
    border-radius: 12px;
    font: inherit;
    transition: opacity 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
  }

  .toolbar-select {
    min-width: 210px;
    padding: 11px 12px;
  }

  .toolbar-year {
    min-width: 90px;
    text-align: center;
  }

  .toolbar-button,
  .toolbar-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 11px 14px;
    text-decoration: none;
    cursor: pointer;
  }

  .toolbar-button:disabled {
    opacity: 0.68;
    cursor: wait;
  }

  .toolbar-button:hover,
  .toolbar-link:hover,
  .toolbar-select:hover {
    opacity: 0.88;
    border-color: rgba(226, 196, 138, 0.55);
  }

  .toolbar-button:active,
  .toolbar-link:active {
    transform: translateY(1px);
  }

  .button-spinner {
    width: 14px;
    height: 14px;
    border: 2px solid rgba(255, 255, 255, 0.28);
    border-top-color: var(--gold-soft);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
  }

  .invoice-card {
    width: min(100%, 680px);
    margin: 0 auto;
    background: var(--card);
    color: #2b2015;
    border-radius: 24px;
    overflow: hidden;
    box-shadow: var(--shadow);
  }

  .invoice-bar {
    height: 12px;
    background: linear-gradient(90deg, #6e522d 0%, var(--gold) 38%, var(--gold-soft) 66%, #7f6035 100%);
  }

  .invoice-body {
    padding: 28px 30px 30px;
  }

  .invoice-heading {
    display: flex;
    justify-content: space-between;
    gap: 22px;
    align-items: flex-start;
    margin-bottom: 24px;
  }

  .invoice-title {
    margin: 0 0 10px;
    color: var(--gold);
    font-family: "Cormorant Garamond", serif;
    font-size: 2.2rem;
    letter-spacing: 0.08em;
  }

  .invoice-meta {
    display: grid;
    gap: 10px;
    min-width: min(280px, 100%);
  }

  .editable-grid {
    display: grid;
    gap: 18px;
    margin-bottom: 24px;
  }

  .grid-two {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 18px;
  }

  .field-group {
    display: grid;
    gap: 6px;
  }

  .field-label {
    color: var(--muted);
    font-size: 0.82rem;
    letter-spacing: 0.05em;
  }

  .field-input {
    width: 100%;
    padding: 7px 0 8px;
    border: none;
    border-bottom: 1px solid var(--line);
    background: transparent;
    color: inherit;
    font: inherit;
    outline: none;
    transition: border-color 0.2s ease, color 0.2s ease;
  }

  .field-input:focus {
    border-bottom-color: var(--gold);
  }

  .field-inline {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
  }

  .field-note {
    font-size: 0.77rem;
    color: var(--muted);
  }

  .amount-banner {
    margin: 28px 0 22px;
    padding: 18px 20px;
    border-radius: 18px;
    background: linear-gradient(135deg, #120c07 0%, var(--banner) 48%, #2a1d10 100%);
    color: var(--paper);
  }

  .amount-banner-label {
    color: rgba(245, 240, 232, 0.7);
    font-size: 0.82rem;
    letter-spacing: 0.08em;
  }

  .amount-banner-value {
    margin: 6px 0 4px;
    color: var(--gold-soft);
    font-family: "Cormorant Garamond", serif;
    font-size: clamp(2rem, 6vw, 3rem);
    line-height: 1;
  }

  .amount-banner-meta {
    color: rgba(245, 240, 232, 0.72);
    font-size: 0.9rem;
    line-height: 1.5;
  }

  .items-table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 18px;
  }

  .items-table th,
  .items-table td {
    padding: 12px 10px;
    border-bottom: 1px solid rgba(181, 145, 90, 0.18);
    text-align: left;
    vertical-align: middle;
  }

  .items-table th {
    color: var(--muted);
    font-size: 0.82rem;
    font-weight: 600;
  }

  .items-table td:last-child,
  .items-table th:last-child {
    text-align: right;
  }

  .item-input {
    width: 100%;
    padding: 6px 0;
    border: none;
    border-bottom: 1px solid transparent;
    background: transparent;
    color: inherit;
    font: inherit;
    outline: none;
    transition: border-color 0.2s ease;
  }

  .item-input:focus {
    border-bottom-color: var(--gold);
  }

  .item-number {
    text-align: right;
  }

  .row-delete {
    border: none;
    background: transparent;
    color: #9a6c4a;
    font-size: 1rem;
    cursor: pointer;
    transition: opacity 0.2s ease;
  }

  .row-delete:hover {
    opacity: 0.66;
  }

  .add-row-button {
    margin: 0 0 22px auto;
    display: inline-flex;
    border: 1px solid rgba(181, 145, 90, 0.28);
    border-radius: 999px;
    background: rgba(181, 145, 90, 0.08);
    color: var(--gold);
    padding: 10px 14px;
    font: inherit;
    cursor: pointer;
    transition: opacity 0.2s ease, transform 0.2s ease;
  }

  .add-row-button:hover {
    opacity: 0.8;
  }

  .add-row-button:active {
    transform: translateY(1px);
  }

  .totals-block {
    width: min(100%, 280px);
    margin: 0 0 0 auto;
    padding-top: 8px;
    display: grid;
    gap: 10px;
  }

  .totals-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    color: #5e4a36;
  }

  .totals-row strong {
    color: #2b2015;
  }

  .totals-row.total {
    margin-top: 4px;
    padding-top: 12px;
    border-top: 1px solid rgba(181, 145, 90, 0.24);
    font-size: 1.02rem;
  }

  .bank-card {
    margin-top: 26px;
    padding: 18px 20px;
    border-radius: 18px;
    background: linear-gradient(180deg, rgba(245, 240, 232, 0.8), rgba(245, 240, 232, 0.46));
    border: 1px solid rgba(181, 145, 90, 0.18);
  }

  .bank-title {
    margin: 0 0 14px;
    color: var(--gold);
    font-family: "Cormorant Garamond", serif;
    font-size: 1.4rem;
    letter-spacing: 0.08em;
  }

  .bank-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 14px;
  }

  .screen-only {
    display: inline;
  }

  .app-footnote {
    max-width: 680px;
    margin: 18px auto 0;
    color: rgba(245, 240, 232, 0.72);
    font-size: 0.86rem;
    line-height: 1.6;
  }

  @keyframes pulse-dot {
    0% {
      box-shadow: 0 0 0 0 rgba(226, 196, 138, 0.55);
    }

    100% {
      box-shadow: 0 0 0 12px rgba(226, 196, 138, 0);
    }
  }

  @keyframes spin {
    to {
      transform: rotate(360deg);
    }
  }

  @media (max-width: 760px) {
    .invoice-heading,
    .grid-two,
    .bank-grid {
      grid-template-columns: 1fr;
      display: grid;
    }

    .invoice-heading {
      gap: 16px;
    }

    .invoice-body {
      padding: 24px 18px;
    }

    .items-table {
      display: block;
      overflow-x: auto;
    }

    .toolbar-select {
      min-width: 100%;
    }
  }

  @media print {
    body {
      background: #ffffff;
    }

    .invoice-editor-shell {
      padding: 0;
      color: #000000;
      background: #ffffff;
    }

    .invoice-toolbar,
    .print-hidden,
    .screen-only,
    .app-footnote {
      display: none !important;
    }

    .invoice-card {
      width: 100%;
      max-width: none;
      border-radius: 0;
      box-shadow: none;
    }

    .field-input,
    .item-input {
      border-bottom-color: transparent !important;
      padding-bottom: 2px;
    }

    .invoice-body {
      padding: 22px 26px;
    }
  }
`;

function InvoiceEditor() {
  const today = new Date();
  const initialMonth = MONTHS[today.getMonth()]?.key ?? "January";
  const initialYear = defaultInvoiceYear(initialMonth, today);

  const [selectedMonth, setSelectedMonth] = useState(initialMonth);
  const [selectedYear, setSelectedYear] = useState(initialYear);
  const [expenseData, setExpenseData] = useState(FALLBACK_EXPENSE_DATA);
  const [syncStatus, setSyncStatus] = useState("idle");
  const [syncNote, setSyncNote] = useState("Using fallback data until sync completes.");
  const [invoice, setInvoice] = useState(() =>
    createInvoiceState(initialMonth, FALLBACK_EXPENSE_DATA, initialYear),
  );
  const [liveDate, setLiveDate] = useState(today);
  const [printDate, setPrintDate] = useState(null);
  const syncMode = getConfiguredSyncMode();

  const rebuildInvoiceForMonth = useCallback((monthKey, year, nextExpenseData) => {
    setInvoice((prev) => {
      const nextState = createInvoiceState(monthKey, nextExpenseData, year);
      return {
        ...prev,
        invoiceNumber: nextState.invoiceNumber,
        subject: nextState.subject,
        dueDate: nextState.dueDate,
        lineItems: nextState.lineItems,
      };
    });
  }, []);

  const syncFromSheets = useCallback(async () => {
    if (syncMode === "fallback") {
      setSyncStatus("idle");
      setExpenseData(FALLBACK_EXPENSE_DATA);
      setSyncNote("No live sync source is configured. Using built-in fallback data.");
      return;
    }

    setSyncStatus("loading");
    setSyncNote(
      syncMode === "proxy"
        ? "Reading monthly totals from the configured sync endpoint..."
        : "Reading Cost Breakdown from Anthropic + Google Drive MCP...",
    );

    try {
      let parsedPayload;

      if (syncMode === "proxy") {
        if (!APP_CONFIG.syncEndpoint) {
          throw new Error("syncEndpoint is missing from window.INVOICE_EDITOR_CONFIG.");
        }

        const syncUrl = new URL(APP_CONFIG.syncEndpoint, window.location.href);
        if (CLIENT_ID) {
          syncUrl.searchParams.set("client", CLIENT_ID);
        }

        const response = await fetch(syncUrl, {
          method: APP_CONFIG.syncMethod || "GET",
          headers: {
            "content-type": "application/json",
            ...(APP_CONFIG.syncHeaders || {}),
          },
        });

        if (!response.ok) {
          throw new Error(`Proxy sync failed with ${response.status}`);
        }

        parsedPayload = extractMonthPayload(await response.json());
      } else {
        if (!GOOGLE_SHEET_ID) {
          throw new Error("googleSheetId is missing from window.INVOICE_EDITOR_CONFIG.");
        }

        // NOTE: this request intentionally carries no API key/auth header.
        // "anthropic" mode only makes sense in a runtime that injects auth
        // for this exact request out-of-band, and only ever should run
        // behind such infrastructure (which also has to solve browser CORS
        // for api.anthropic.com, since raw browser requests cannot succeed
        // otherwise). Never hardcode an API key here or in
        // invoice-config.js — that would ship the key to every browser that
        // loads this page. Use "proxy" mode with a real backend for any
        // normal deployment.
        const requestBody = {
          model: "claude-sonnet-4-20250514",
          max_tokens: 1600,
          messages: [
            {
              role: "user",
              content: [
                {
                  type: "text",
                  text:
                    `Use the Google Drive MCP server to read the "Cost Breakdown" sheet from spreadsheet id ${GOOGLE_SHEET_ID}. ` +
                    `Return JSON only with exactly these 12 top-level keys: January, February, March, April, May, June, July, August, September, October, November, December. ` +
                    `Each month value must be an object whose keys are category names and whose values are numeric totals for that month. ` +
                    `Only include categories whose amount is greater than 0. Do not include markdown, prose, or code fences.`,
                },
              ],
            },
          ],
          mcp_servers: [
            {
              type: "url",
              url: "https://drivemcp.googleapis.com/mcp/v1",
              name: "gdrive",
            },
          ],
          tools: [
            {
              type: "mcp_toolset",
              mcp_server_name: "gdrive",
            },
          ],
        };

        const response = await fetch(
          APP_CONFIG.anthropicApiUrl || "https://api.anthropic.com/v1/messages",
          {
            method: "POST",
            headers: {
              "content-type": "application/json",
              "anthropic-version": "2023-06-01",
              "anthropic-beta": "mcp-client-2025-11-20",
            },
            body: JSON.stringify(requestBody),
          },
        );

        if (!response.ok) {
          throw new Error(`Anthropic request failed with ${response.status}`);
        }

        const payload = await response.json();
        const textPayload = extractJsonText(payload);
        parsedPayload = JSON.parse(textPayload);
      }

      const normalizedExpenses = normalizeExpenseShape(parsedPayload);

      setExpenseData(normalizedExpenses);
      setSyncStatus("ok");
      setSyncNote(
        `${SYNC_MODE_LABELS[syncMode]} completed at ${formatDate(new Date(), true)}.`,
      );
    } catch (error) {
      setSyncStatus("error");
      setExpenseData(FALLBACK_EXPENSE_DATA);
      setSyncNote(`${SYNC_MODE_LABELS[syncMode]} failed. The editor is using fallback data.`);
      console.error(error);
    }
  }, [syncMode]);

  useEffect(() => {
    const timerId = window.setInterval(() => {
      setLiveDate(new Date());
    }, 1000);

    return () => {
      window.clearInterval(timerId);
    };
  }, []);

  useEffect(() => {
    const handleBeforePrint = () => {
      setPrintDate(new Date());
    };

    const handleAfterPrint = () => {
      setPrintDate(null);
      setLiveDate(new Date());
    };

    window.addEventListener("beforeprint", handleBeforePrint);
    window.addEventListener("afterprint", handleAfterPrint);

    return () => {
      window.removeEventListener("beforeprint", handleBeforePrint);
      window.removeEventListener("afterprint", handleAfterPrint);
    };
  }, []);

  useEffect(() => {
    rebuildInvoiceForMonth(selectedMonth, selectedYear, expenseData);
  }, [expenseData, rebuildInvoiceForMonth, selectedMonth, selectedYear]);

  useEffect(() => {
    syncFromSheets();
  }, [syncFromSheets]);

  const handleMonthChange = useCallback((event) => {
    setSelectedMonth(event.target.value);
  }, []);

  const handleYearChange = useCallback((event) => {
    const parsed = Number.parseInt(event.target.value, 10);
    if (Number.isFinite(parsed)) {
      setSelectedYear(parsed);
    }
  }, []);

  const handleInvoiceFieldChange = useCallback((field, value) => {
    setInvoice((prev) => ({
      ...prev,
      [field]: value,
    }));
  }, []);

  const updateLineItem = useCallback((itemId, field, value) => {
    setInvoice((prev) => ({
      ...prev,
      lineItems: prev.lineItems.map((item) => {
        if (item.id !== itemId) {
          return item;
        }

        const nextItem = { ...item, [field]: value };

        if (field === "quantity" || field === "unitPrice") {
          const quantity = parseNumericInput(field === "quantity" ? value : nextItem.quantity);
          const unitPrice = parseNumericInput(field === "unitPrice" ? value : nextItem.unitPrice);

          nextItem.amount =
            Number.isFinite(quantity) && Number.isFinite(unitPrice)
              ? toNumericString(quantity * unitPrice)
              : "";
        }

        if (field === "amount" && !String(value).trim()) {
          nextItem.amount = "";
        }

        return nextItem;
      }),
    }));
  }, []);

  const addRow = useCallback(() => {
    setInvoice((prev) => ({
      ...prev,
      lineItems: [...prev.lineItems, toLineItem("", "1", "", "")],
    }));
  }, []);

  const removeRow = useCallback((itemId) => {
    setInvoice((prev) => {
      const nextItems = prev.lineItems.filter((item) => item.id !== itemId);

      return {
        ...prev,
        lineItems: nextItems.length > 0 ? nextItems : [toLineItem("", "1", "", "")],
      };
    });
  }, []);

  const handlePrint = useCallback(() => {
    setPrintDate(new Date());
    window.print();
  }, []);

  const total = invoice.lineItems.reduce((sum, item) => {
    const numericAmount = parseNumericInput(item.amount);
    return sum + (Number.isFinite(numericAmount) ? numericAmount : 0);
  }, 0);
  const subtotal = total / (1 + TAX_RATE);
  const tax = total - subtotal;
  const bannerNote = [
    monthLabelFromKey(selectedMonth),
    SYNC_MODE_LABELS[syncMode],
    STATUS_LABELS[syncStatus],
    CLIENT_ID ? `client: ${CLIENT_ID}` : null,
  ]
    .filter(Boolean)
    .join(" / ");
  const displayedDate = printDate ?? liveDate;

  return (
    <>
      <style>{componentStyles}</style>
      <div className="invoice-editor-shell">
        <div className="invoice-toolbar print-hidden">
          <h1 className="toolbar-title">請求書エディター</h1>
          <div className="toolbar-group">
            <div className={`status-pill ${syncStatus}`}>
              <span className="status-dot" />
              <span>{STATUS_LABELS[syncStatus]}</span>
            </div>

            <select
              className="toolbar-select"
              value={selectedMonth}
              onChange={handleMonthChange}
              aria-label="Invoice month"
            >
              {MONTHS.map((month) => (
                <option key={month.key} value={month.key}>
                  {`${month.label} — ${formatCurrency(monthTotal(expenseData, month.key))}`}
                </option>
              ))}
            </select>

            <input
              type="number"
              className="toolbar-select toolbar-year"
              value={selectedYear}
              onChange={handleYearChange}
              aria-label="Invoice year"
            />

            <button
              type="button"
              className="toolbar-button"
              onClick={syncFromSheets}
              disabled={syncStatus === "loading"}
            >
              {syncStatus === "loading" ? <span className="button-spinner" /> : "↻"}
              Sheets 同期
            </button>

            {GOOGLE_SHEET_URL && (
              <a className="toolbar-link" href={GOOGLE_SHEET_URL} target="_blank" rel="noreferrer">
                ↗ スプレッドシートを開く
              </a>
            )}

            <button type="button" className="toolbar-button" onClick={handlePrint}>
              PDF 出力 / 印刷
            </button>
          </div>
        </div>

        <article className="invoice-card">
          <div className="invoice-bar" />
          <div className="invoice-body">
            <header className="invoice-heading">
              <div>
                <h2 className="invoice-title">請求書</h2>
                <div className="field-group">
                  <span className="field-label">請求番号</span>
                  <input
                    className="field-input"
                    value={invoice.invoiceNumber}
                    onChange={(event) => handleInvoiceFieldChange("invoiceNumber", event.target.value)}
                  />
                </div>
              </div>

              <div className="invoice-meta">
                <div className="field-group">
                  <span className="field-label">請求日</span>
                  <div className="field-inline">
                    <input className="field-input" value={formatDate(displayedDate, Boolean(printDate))} readOnly />
                    <span className="field-note screen-only">(出力時に自動更新)</span>
                  </div>
                </div>
              </div>
            </header>

            <section className="editable-grid">
              <div className="grid-two">
                <div className="field-group">
                  <span className="field-label">請求者</span>
                  <input
                    className="field-input"
                    value={invoice.fromName}
                    onChange={(event) => handleInvoiceFieldChange("fromName", event.target.value)}
                  />
                  <input
                    className="field-input"
                    value={invoice.fromAddress1}
                    onChange={(event) => handleInvoiceFieldChange("fromAddress1", event.target.value)}
                  />
                  <input
                    className="field-input"
                    value={invoice.fromAddress2}
                    onChange={(event) => handleInvoiceFieldChange("fromAddress2", event.target.value)}
                  />
                </div>

                <div className="field-group">
                  <span className="field-label">請求先</span>
                  <input
                    className="field-input"
                    value={invoice.toName}
                    onChange={(event) => handleInvoiceFieldChange("toName", event.target.value)}
                  />
                  <span className="field-label">件名</span>
                  <input
                    className="field-input"
                    value={invoice.subject}
                    onChange={(event) => handleInvoiceFieldChange("subject", event.target.value)}
                  />
                  <span className="field-label">お支払期限</span>
                  <input
                    className="field-input"
                    value={invoice.dueDate}
                    onChange={(event) => handleInvoiceFieldChange("dueDate", event.target.value)}
                  />
                </div>
              </div>
            </section>

            <section className="amount-banner">
              <div className="amount-banner-label">ご請求金額</div>
              <div className="amount-banner-value">{formatCurrency(total)}</div>
              <div className="amount-banner-meta">
                {bannerNote}
                {" / "}
                {syncNote}
              </div>
            </section>

            <table className="items-table">
              <thead>
                <tr>
                  <th>品名</th>
                  <th>数量</th>
                  <th>単価</th>
                  <th>金額</th>
                  <th className="print-hidden" />
                </tr>
              </thead>
              <tbody>
                {invoice.lineItems.map((item) => (
                  <tr key={item.id}>
                    <td>
                      <input
                        className="item-input"
                        aria-label="品名"
                        value={item.description}
                        onChange={(event) => updateLineItem(item.id, "description", event.target.value)}
                      />
                    </td>
                    <td>
                      <input
                        className="item-input item-number"
                        inputMode="decimal"
                        aria-label="数量"
                        value={item.quantity}
                        onChange={(event) => updateLineItem(item.id, "quantity", event.target.value)}
                      />
                    </td>
                    <td>
                      <input
                        className="item-input item-number"
                        inputMode="decimal"
                        aria-label="単価"
                        value={item.unitPrice}
                        onChange={(event) => updateLineItem(item.id, "unitPrice", event.target.value)}
                      />
                    </td>
                    <td>
                      <input
                        className="item-input item-number"
                        inputMode="decimal"
                        aria-label="金額"
                        value={item.amount}
                        onChange={(event) => updateLineItem(item.id, "amount", event.target.value)}
                      />
                    </td>
                    <td className="print-hidden">
                      <button
                        type="button"
                        className="row-delete"
                        onClick={() => removeRow(item.id)}
                        aria-label="Delete row"
                      >
                        ×
                      </button>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>

            <button type="button" className="add-row-button print-hidden" onClick={addRow}>
              ＋ 行を追加
            </button>

            <section className="totals-block">
              <div className="totals-row">
                <span>小計</span>
                <span>{formatCurrency(subtotal)}</span>
              </div>
              <div className="totals-row">
                <span>消費税 (10% 内税)</span>
                <span>{formatCurrency(tax)}</span>
              </div>
              <div className="totals-row total">
                <strong>合計</strong>
                <strong>{formatCurrency(total)}</strong>
              </div>
            </section>

            <section className="bank-card">
              <h3 className="bank-title">お振込先</h3>
              <div className="bank-grid">
                <div className="field-group">
                  <span className="field-label">銀行名</span>
                  <input
                    className="field-input"
                    value={invoice.bankName}
                    onChange={(event) => handleInvoiceFieldChange("bankName", event.target.value)}
                  />
                </div>
                <div className="field-group">
                  <span className="field-label">支店</span>
                  <input
                    className="field-input"
                    value={invoice.bankBranch}
                    onChange={(event) => handleInvoiceFieldChange("bankBranch", event.target.value)}
                  />
                </div>
                <div className="field-group">
                  <span className="field-label">口座番号</span>
                  <input
                    className="field-input"
                    value={invoice.bankAccount}
                    onChange={(event) => handleInvoiceFieldChange("bankAccount", event.target.value)}
                  />
                </div>
              </div>
            </section>
          </div>
          <div className="invoice-bar" />
        </article>

        <p className="app-footnote">
          This standalone page uses React in the browser. Live sync is configurable through
          <code> invoice-config.js </code>
          and can run through a proxy endpoint or an Anthropic artifact-style MCP request. If no live
          sync source is configured, the editor keeps working with the built-in fallback monthly data.
        </p>
      </div>
    </>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<InvoiceEditor />);
