/**
 * src/services/approvalService.js
 *
 * Handles the "WAITING FOR APPROVAL" detection + Adaptive Card (v1.3) UX.
 * - Fetches proposals + current NTE from ServiceChannel.
 * - Posts markdown itemization summary + approval card.
 * - Processes card submits: reject superseded proposals, approve, optional NTE override.
 */

import { logger } from '../utils/logger.js';
import {
  getWorkOrderForNte,
  updateWorkOrderNte,
  getProposalsAssociatedWithWorkOrder,
  getProposalByIdOdata,
  getProposalByNumberOdata,
  getProposalsToReject,
  resolveRejectReasonCodeId,
  rejectProposal,
  approveProposal,
} from '../integrations/serviceChannel/client.js';
import webexService from './webexService.js';

const CARD_DEDUP_TTL_MS = 5 * 60 * 1000;
const _recentApprovalCards = new Map();

function _pruneDedupCache(now = Date.now()) {
  for (const [k, ts] of _recentApprovalCards.entries()) {
    if (now - ts > CARD_DEDUP_TTL_MS) _recentApprovalCards.delete(k);
  }
}

function _extractProposalNumber(noteData) {
  if (!noteData) return null;
  const m = String(noteData).match(/Proposal\s*#?(\d+)/i);
  return m ? m[1] : null;
}

function _dedupKeys(woId, noteData) {
  const keys = [`wo:${woId}:pending`];
  const p = _extractProposalNumber(noteData);
  if (p) keys.push(`wo:${woId}:p:${p}`);
  return keys;
}

function _proposalStatusPrimary(p) {
  if (!p) return '';
  if (typeof p.Status === 'string') return p.Status;
  return p.Status?.Primary || '';
}

function _isApproved(p) {
  return _proposalStatusPrimary(p).toLowerCase() === 'approved';
}

function _proposalRefNumber(p) {
  return String(p?.Number ?? p?.ProposalNumber ?? p?.ID ?? p?.Id ?? '');
}

function _proposalRefId(p) {
  return p?.Id ?? p?.ID ?? null;
}

function _proposalDisplayNumber(p) {
  return p?.Number ?? p?.ProposalNumber ?? _proposalRefId(p) ?? '?';
}

function _proposalDetailsUrl(proposal) {
  const id = _proposalRefId(proposal);
  if (!id) return null;
  return `https://www.servicechannel.com/proposal/details/${id}`;
}

function _formatMoney(n) {
  const v = Number(n);
  return Number.isFinite(v) ? `$${v.toFixed(2)}` : '$0.00';
}

function _filterCategories(categories = []) {
  return categories.filter((cat) => {
    const cost = cat.TotalCost;
    if (cost == null) return false;
    const name = (cat.Name || '').toLowerCase();
    if (name.includes('costs incurred to date')) return false;
    return true;
  });
}

/**
 * Extract line-item arrays from a proposal object (best-effort).
 */
export function extractLineItems(proposal) {
  if (!proposal) return [];
  const candidates = [
    proposal.Items,
    proposal.LineItems,
    proposal.ProposalItems,
    proposal.items,
    proposal.Charges,
    proposal.Materials,
  ];
  for (const arr of candidates) {
    if (Array.isArray(arr) && arr.length > 0) return arr;
  }
  return [];
}

function _lineItemPart(it) {
  return it.PartNum || it.PartNumber || it.SKU || it.Code || it.Name || it.Description || '—';
}

function _lineItemQty(it) {
  const q = it.Quantity ?? it.Qty ?? it.NumOfTech;
  return q != null ? String(q) : '—';
}

function _lineItemUnitPrice(it) {
  const p = it.UnitPrice ?? it.HourlyRate ?? it.Rate;
  return p != null ? _formatMoney(p) : '—';
}

function _lineItemTotal(it) {
  const t = it.Amount ?? it.Cost ?? it.Total ?? it.Value;
  return t != null ? _formatMoney(t) : '—';
}

/**
 * SC-invoice-style markdown summary for a proposal.
 */
export function formatProposalMarkdown(wo, proposal, context = {}) {
  if (!proposal) {
    return `**Proposal Approval Required** — WO-${wo?.Number || wo?.Id || '???'}\n\nNo proposal details available.`;
  }

  const woNum = wo?.Number || wo?.Id || '???';
  const woLink = `https://www.servicechannel.com/sc/wo/Workorders/index?id=${wo?.Id || woNum}`;
  const store = wo?.LocationStoreId != null && wo?.LocationStoreId !== ''
    ? `Store ${wo.LocationStoreId}`
    : (wo?.LocationName || '');
  const provider = wo?.ProviderName || context.providerName || '';
  const pNum = _proposalDisplayNumber(proposal);
  const proposalDetailsUrl = _proposalDetailsUrl(proposal);
  const pStatus = typeof proposal.Status === 'object'
    ? `${proposal.Status.Primary || ''}${proposal.Status.Extended ? ` | ${proposal.Status.Extended}` : ''}`
    : (proposal.Status || 'Open');
  const created = proposal.CreatedDate
    ? new Date(proposal.CreatedDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
    : '';
  const desc = (proposal.Description || proposal.Description2 || proposal.Comments || '').toString().trim();
  const total = Number(proposal.Amount ?? proposal.Total ?? proposal.TotalAmount ?? 0);

  let md = `## Proposal #${pNum}\n\n`;
  if (proposalDetailsUrl) {
    md += `[View full proposal details in ServiceChannel](${proposalDetailsUrl})\n\n`;
  }
  md += `[WO-${woNum}](${woLink})`;
  if (store) md += ` | ${store}`;
  if (provider) md += ` | ${provider}`;
  md += '\n\n';
  if (created) md += `**Created:** ${created}  \n`;
  md += `**Status:** ${pStatus}  \n`;
  if (desc) md += `**Description:** ${desc}\n`;
  md += '\n';

  const lineItems = extractLineItems(proposal);
  if (lineItems.length > 0) {
    md += '### Materials / Line Items\n\n';
    md += '| Part | Qty | Unit Price | Total |\n';
    md += '| --- | ---: | ---: | ---: |\n';
    for (const it of lineItems) {
      md += `| ${_lineItemPart(it)} | ${_lineItemQty(it)} | ${_lineItemUnitPrice(it)} | ${_lineItemTotal(it)} |\n`;
    }
    md += '\n';
  }

  const categories = _filterCategories(proposal.AmountCategories || []);
  if (categories.length > 0 || total > 0) {
    md += '### Summary\n\n';
    md += '| Category | Amount |\n';
    md += '| --- | ---: |\n';
    for (const cat of categories) {
      md += `| ${cat.Name || 'Category'} | ${_formatMoney(cat.TotalCost)} |\n`;
    }
    if (total > 0) {
      md += `| **Total** | **${_formatMoney(total)}** |\n`;
    }
    md += '\n';
  }

  const toReject = context.proposalsToReject || [];
  if (toReject.length > 0) {
    const list = toReject.map((p) => `#${p.number || p.id} (${_formatMoney(p.amount)})`).join(', ');
    md += `> **Note:** Approving this proposal will reject prior proposal(s): ${list}\n`;
  }

  return md.trim();
}

function _buildCategoryColumnSet(categories, total) {
  const rows = [];
  const header = {
    type: 'ColumnSet',
    columns: [
      { type: 'Column', width: 'stretch', items: [{ type: 'TextBlock', text: 'Category', weight: 'bolder', size: 'small' }] },
      { type: 'Column', width: 'auto', items: [{ type: 'TextBlock', text: 'Amount', weight: 'bolder', size: 'small', horizontalAlignment: 'right' }] },
    ],
  };
  rows.push(header);

  for (const cat of categories) {
    rows.push({
      type: 'ColumnSet',
      spacing: 'none',
      columns: [
        { type: 'Column', width: 'stretch', items: [{ type: 'TextBlock', text: cat.Name || 'Category', size: 'small', wrap: true }] },
        { type: 'Column', width: 'auto', items: [{ type: 'TextBlock', text: _formatMoney(cat.TotalCost), size: 'small', horizontalAlignment: 'right' }] },
      ],
    });
  }

  if (total > 0) {
    rows.push({
      type: 'ColumnSet',
      separator: true,
      spacing: 'small',
      columns: [
        { type: 'Column', width: 'stretch', items: [{ type: 'TextBlock', text: 'Total', weight: 'bolder', size: 'small' }] },
        { type: 'Column', width: 'auto', items: [{ type: 'TextBlock', text: _formatMoney(total), weight: 'bolder', size: 'small', horizontalAlignment: 'right' }] },
      ],
    });
  }

  return rows;
}

function _normalizeProposalsToReject(raw = []) {
  return raw.map((p) => ({
    id: _proposalRefId(p),
    number: p.Number || p.ProposalNumber || p.ID || p.Id,
    amount: Number(p.Amount ?? p.Total ?? p.TotalAmount ?? 0),
  })).filter((p) => p.id);
}

/**
 * Pick the pending proposal from associated list.
 * Prefers note-parsed proposal #, else newest non-approved by CreatedDate.
 */
export function selectPendingProposal(associated = [], parsedProposalNumber = null) {
  if (!associated.length) return null;

  if (parsedProposalNumber) {
    const match = associated.find((p) => _proposalRefNumber(p) === String(parsedProposalNumber));
    if (match) return match;
  }

  const pending = associated.filter((p) => !_isApproved(p));
  const pool = pending.length > 0 ? pending : associated;

  const sorted = [...pool].sort((a, b) => {
    const da = new Date(a.CreatedDate || a.CreatedDate_dto || 0).getTime();
    const db = new Date(b.CreatedDate || b.CreatedDate_dto || 0).getTime();
    return db - da;
  });

  return sorted[0] || null;
}

/**
 * Build a v1.3 Adaptive Card for proposal approval.
 */
export function buildApprovalAdaptiveCard(wo, proposals = [], currentNte = 0, options = {}) {
  const { proposalsToReject = [] } = options;
  const woNum = wo?.Number || wo?.Id || '???';
  const woLink = `https://www.servicechannel.com/sc/wo/Workorders/index?id=${wo?.Id || woNum}`;
  const store = wo?.LocationStoreId ? `Store ${wo.LocationStoreId}` : (wo?.LocationName || '');
  const status = `${wo?.Status?.Primary || 'IN PROGRESS'} | ${wo?.Status?.Extended || 'WAITING FOR APPROVAL'}`;

  const proposal = proposals?.length > 0 ? proposals[0] : null;

  let proposalAmount = 0;
  if (proposal) {
    proposalAmount = Number(
      proposal.Amount ?? proposal.Total ?? proposal.TotalAmount ?? proposal.Cost ?? proposal.Value ?? proposal.Nte ?? 0
    );
  }

  const items = extractLineItems(proposal);
  if (!proposalAmount && items.length > 0) {
    proposalAmount = items.reduce((sum, it) => sum + Number(it.Amount || it.Cost || it.Total || it.Value || 0), 0);
  }

  const suggestedNte = proposalAmount;

  const body = [
    {
      type: 'TextBlock',
      text: 'Proposal Approval Required',
      size: 'medium',
      weight: 'bolder',
    },
    {
      type: 'TextBlock',
      text: `[WO-${woNum}](${woLink})  •  ${store}`,
      isSubtle: true,
      wrap: true,
    },
    {
      type: 'FactSet',
      facts: [
        { title: 'Current Status', value: status },
        { title: 'Current NTE', value: currentNte ? _formatMoney(currentNte) : 'N/A' },
      ],
    },
  ];

  if (proposalsToReject.length > 0) {
    const list = proposalsToReject.map((p) => `#${p.number || p.id} (${_formatMoney(p.amount)})`).join(', ');
    body.push({
      type: 'TextBlock',
      text: `Approving will reject prior proposal(s): ${list}`,
      color: 'warning',
      weight: 'bolder',
      wrap: true,
      spacing: 'medium',
    });
  }

  if (proposal) {
    const pNum = _proposalDisplayNumber(proposal);
    const proposalDetailsUrl = _proposalDetailsUrl(proposal);
    body.push({
      type: 'TextBlock',
      text: `Proposal #${pNum}`,
      weight: 'bolder',
      spacing: 'medium',
    });

    if (proposalDetailsUrl) {
      body.push({
        type: 'TextBlock',
        text: `[View full proposal details in ServiceChannel](${proposalDetailsUrl})`,
        size: 'small',
        wrap: true,
        spacing: 'small',
      });
    }

    if (proposalAmount > 0) {
      body.push({
        type: 'TextBlock',
        text: `Proposal total (new NTE): **${_formatMoney(proposalAmount)}**`,
        weight: 'bolder',
        color: 'attention',
      });
    }

    const pDesc = (proposal.Description || proposal.Description2 || proposal.Comments || '').toString().substring(0, 200);
    if (pDesc) {
      body.push({
        type: 'TextBlock',
        text: pDesc + (pDesc.length >= 200 ? '…' : ''),
        size: 'small',
        wrap: true,
        isSubtle: true,
      });
    }

    const categories = _filterCategories(proposal.AmountCategories || []);
    if (categories.length > 0) {
      body.push({
        type: 'TextBlock',
        text: 'Proposal Charges',
        weight: 'bolder',
        spacing: 'medium',
      });
      body.push(..._buildCategoryColumnSet(categories, proposalAmount));
    } else if (proposalAmount > 0) {
      body.push({
        type: 'TextBlock',
        text: `Total Proposal Amount: ${_formatMoney(proposalAmount)} (detailed categories not available)`,
        size: 'small',
        isSubtle: true,
        wrap: true,
      });
    }
  } else {
    body.push({
      type: 'TextBlock',
      text: 'No specific proposal details found. You can still adjust the NTE manually below.',
      wrap: true,
      isSubtle: true,
    });
  }

  body.push(
    {
      type: 'TextBlock',
      text: 'New NTE Amount (defaults to proposal total; adjust if needed)',
      weight: 'bolder',
      spacing: 'medium',
    },
    {
      type: 'Input.Number',
      id: 'newNte',
      value: suggestedNte || 0,
      placeholder: 'Enter final approved NTE',
      min: 0,
    },
    {
      type: 'TextBlock',
      text: 'Optional comment / additional instructions (will be recorded with the approval)',
      spacing: 'small',
    },
    {
      type: 'Input.Text',
      id: 'comment',
      placeholder: 'e.g. Approved as quoted + $75 for expedited parts',
      isMultiline: true,
    }
  );

  return {
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    type: 'AdaptiveCard',
    version: '1.3',
    body,
    actions: [
      {
        type: 'Action.Submit',
        title: 'Approve Proposal',
        data: {
          action: 'approveNte',
          workOrderId: wo?.Id || woNum,
          proposalId: proposal?.Id || proposal?.ID || null,
          proposalNumber: proposal?.Number || proposal?.ProposalNumber || null,
          suggestedNte,
          currentNte: Number(currentNte) || 0,
          proposalAmount,
          proposalsToRejectJson: JSON.stringify(proposalsToReject),
        },
      },
      {
        type: 'Action.Submit',
        title: 'Cancel',
        associatedInputs: 'none',
        data: {
          action: 'dismissApprovalCard',
          workOrderId: wo?.Id || woNum,
          proposalId: proposal?.Id || proposal?.ID || null,
        },
      },
    ],
  };
}

/**
 * Shared flow: fetch proposal data, post markdown summary + approval card.
 */
export async function postApprovalPackage(webexClient, roomId, woObj, noteData = null, { skipDedup = false, db = null } = {}) {
  if (!roomId || !woObj) return;

  const woId = woObj.Id;
  const now = Date.now();

  if (!skipDedup) {
    _pruneDedupCache(now);
    const dedupKeys = _dedupKeys(woId, noteData);
    for (const k of dedupKeys) {
      const lastPostedAt = _recentApprovalCards.get(k);
      if (lastPostedAt && (now - lastPostedAt) < CARD_DEDUP_TTL_MS) {
        logger('approval', `Skipping duplicate approval card for WO ${woId} (posted ${Math.round((now - lastPostedAt) / 1000)}s ago, key=${k})`);
        return;
      }
    }
    for (const k of dedupKeys) _recentApprovalCards.set(k, now);
  }

  const dedupKeys = _dedupKeys(woId, noteData);

  try {
    const parsedProposalNumber = _extractProposalNumber(noteData);

    const [associated, proposalsToRejectRaw, woDetails] = await Promise.all([
      getProposalsAssociatedWithWorkOrder(woId),
      getProposalsToReject(woId),
      getWorkOrderForNte(woId),
    ]);

    logger('approval', `Found ${associated.length} associated proposals for WO ${woId}; ${proposalsToRejectRaw.length} to reject`);

    let selectedProposal = null;
    let proposalIdForLog = null;

    if (associated.length > 0) {
      const target = selectPendingProposal(associated, parsedProposalNumber);
      const pid = target ? (target.ID || target.Id) : null;
      if (pid) {
        selectedProposal = await getProposalByIdOdata(pid);
        proposalIdForLog = pid;
      }
    } else if (parsedProposalNumber) {
      logger('approval', `No associated proposals for WO ${woId}; trying OData by proposal #${parsedProposalNumber}`);
      selectedProposal = await getProposalByNumberOdata(parsedProposalNumber);
      if (selectedProposal) {
        proposalIdForLog = selectedProposal.Id || selectedProposal.ID || parsedProposalNumber;
      }
    }

    const proposalsToReject = _normalizeProposalsToReject(proposalsToRejectRaw)
      .filter((p) => !proposalIdForLog || String(p.id) !== String(proposalIdForLog));
    const currentNte = woDetails?.Nte ?? woObj?.Nte ?? 0;
    const woForDisplay = { ...woObj, ...woDetails };

    const sender = webexClient && typeof webexClient.sendAdaptiveCard === 'function'
      ? webexClient
      : webexService;

    const markdown = formatProposalMarkdown(woForDisplay, selectedProposal, { proposalsToReject });
    await sender.sendMarkdown(roomId, markdown);

    const card = buildApprovalAdaptiveCard(
      woForDisplay,
      selectedProposal ? [selectedProposal] : [],
      currentNte,
      { proposalsToReject }
    );

    const proposalAmountForFallback = selectedProposal
      ? Number(selectedProposal.Amount || selectedProposal.Total || selectedProposal.TotalAmount || 0)
      : 0;

    const fallback = `Approval card for WO-${woObj.Number || woId}. Proposal amount: ${_formatMoney(proposalAmountForFallback)}. Suggested NTE: ${_formatMoney(proposalAmountForFallback)}.`;

    if (db) {
      const existing = await getPendingApprovalCard(db, woId);
      if (existing?.messageId) {
        await deleteApprovalCardMessage(existing.messageId);
      }
    }

    const cardMsg = await sender.sendAdaptiveCard(roomId, card, fallback);
    if (db && cardMsg?.id) {
      await savePendingApprovalCard(db, {
        workOrderId: woId,
        roomId,
        messageId: cardMsg.id,
        proposalId: proposalIdForLog,
      });
    }
    logger('approval', `Posted approval package for WO-${woObj.Number || woId} in room ${roomId} (proposalId=${proposalIdForLog || 'n/a'}, reject=${proposalsToReject.length}, messageId=${cardMsg?.id || 'n/a'})`);
  } catch (err) {
    if (!skipDedup) {
      for (const k of dedupKeys) _recentApprovalCards.delete(k);
    }
    logger('approval', `Failed to post approval package for WO ${woId}: ${err.message}`, 'error');
    try {
      const md = `**Action Required: Proposal Approval for WO-${woObj?.Number || woId}**\n\nStatus: ${woObj?.Status?.Primary} | ${woObj?.Status?.Extended}\n\nPlease review proposals in ServiceChannel and update NTE manually.`;
      const sender = webexClient?.sendMarkdown ? webexClient : webexService;
      await sender.sendMarkdown(roomId, md);
    } catch (_) {}
    throw err;
  }
}

export async function fetchAndPostApprovalCardIfNeeded(webexClient, roomId, woObj, noteData = null, { db = null } = {}) {
  return postApprovalPackage(webexClient, roomId, woObj, noteData, { db });
}

function _parseProposalsToRejectFromSubmit(inputs) {
  if (inputs.proposalsToRejectJson) {
    try {
      const parsed = JSON.parse(inputs.proposalsToRejectJson);
      if (Array.isArray(parsed)) return parsed;
    } catch (_) {}
  }
  return [];
}

function _isAlreadyRejectedError(msg) {
  const m = String(msg).toLowerCase();
  return m.includes('already rejected') || m.includes('was already rejected');
}

function _isProposalResolved(noteData, woObj) {
  const note = String(noteData || '');
  const ext = (woObj?.Status?.Extended || '').toUpperCase();
  if (/Proposal\s*#?\s*(\d+)\s+has been approved/i.test(note)) return true;
  if (/Proposal\(s\)\s*#\s*(\d+)\s+has been rejected/i.test(note)) return true;
  if (ext.includes('PROPOSAL APPROVED')) return true;
  return false;
}

function getPendingApprovalCard(db, workOrderId) {
  return new Promise((resolve, reject) => {
    db.get(
      'SELECT workOrderId, roomId, messageId, proposalId, postedAt FROM pending_approval_cards WHERE workOrderId = ?',
      [workOrderId],
      (err, row) => (err ? reject(err) : resolve(row || null))
    );
  });
}

function savePendingApprovalCard(db, { workOrderId, roomId, messageId, proposalId }) {
  return new Promise((resolve, reject) => {
    db.run(
      `INSERT OR REPLACE INTO pending_approval_cards (workOrderId, roomId, messageId, proposalId, postedAt)
       VALUES (?, ?, ?, ?, ?)`,
      [workOrderId, roomId, messageId, proposalId ?? null, new Date().toISOString()],
      (err) => (err ? reject(err) : resolve())
    );
  });
}

function clearPendingApprovalCard(db, workOrderId) {
  return new Promise((resolve, reject) => {
    db.run(
      'DELETE FROM pending_approval_cards WHERE workOrderId = ?',
      [workOrderId],
      (err) => (err ? reject(err) : resolve())
    );
  });
}

async function deleteApprovalCardMessage(messageId) {
  if (!messageId) return;
  try {
    const botClientMod = await import('../integrations/webex/botClient.js');
    await botClientMod.default.deleteMessage(messageId);
    logger('approval', `Deleted approval card message ${messageId}`);
  } catch (err) {
    logger('approval', `Could not delete approval card ${messageId}: ${err.message}`, 'warn');
  }
}

/** Remove the adaptive card message after a successful approval or dismiss. */
async function _removeApprovalCard(bot, action) {
  const messageId = action?.messageId;
  if (!messageId) return;

  if (typeof bot?.censor === 'function') {
    try {
      await bot.censor(messageId);
      logger('approval:submit', `Removed approval card message ${messageId}`);
      return;
    } catch (err) {
      logger('approval:submit', `bot.censor failed for ${messageId}: ${err.message}`, 'warn');
    }
  }

  try {
    const botClientMod = await import('../integrations/webex/botClient.js');
    await botClientMod.default.deleteMessage(messageId);
    logger('approval:submit', `Removed approval card message ${messageId} via API`);
  } catch (err) {
    logger('approval:submit', `Could not remove approval card ${messageId}: ${err.message}`, 'warn');
  }
}

/**
 * Remove stored approval card when proposal was approved/rejected in ServiceChannel.
 */
export async function removeApprovalCardIfResolved({ db, workOrderId, woObj, noteData }) {
  if (!db || !workOrderId) return;
  if (!_isProposalResolved(noteData, woObj)) return;

  try {
    const row = await getPendingApprovalCard(db, workOrderId);
    if (!row?.messageId) return;

    await deleteApprovalCardMessage(row.messageId);
    await clearPendingApprovalCard(db, workOrderId);
    logger('approval', `Removed stale approval card for WO ${workOrderId} (resolved in ServiceChannel)`);
  } catch (err) {
    logger('approval', `Failed to remove resolved approval card for WO ${workOrderId}: ${err.message}`, 'warn');
  }
}

async function _rejectSupersededProposals(woId, proposalsToReject, approver, approverEmail, newProposalNumber) {
  if (!proposalsToReject.length) return { rejected: [], failed: [] };

  const reasonCodeId = await resolveRejectReasonCodeId();
  const rejected = [];
  const failed = [];

  for (const p of proposalsToReject) {
    const pid = p.id;
    if (!pid) continue;

    const rejectBody = {
      Comments: `Superseded by proposal #${newProposalNumber || 'new'} — rejected via ServChan by ${approver}`,
      ProviderEmail: '',
      UserEmail: '',
      RejectReasonCodeId: reasonCodeId,
      ActionSource: 'Standard',
      ReasonString: `Superseded by revised proposal — rejected via ServChan by ${approver}`,
      PinNote: false,
    };

    try {
      await rejectProposal(pid, rejectBody);
      rejected.push(p);
      logger('approval:submit', `Rejected superseded proposal ${pid} (#${p.number}) for WO ${woId}`);
    } catch (err) {
      const msg = err.message || err.toString();
      const scCode = err.response?.data?.ErrorCode;
      if (_isAlreadyRejectedError(msg)) {
        logger('approval:submit', `Proposal ${pid} already rejected — skipping`, 'warn');
        rejected.push(p);
      } else {
        failed.push({ ...p, error: msg, scCode });
        logger('approval:submit', `Failed to reject proposal ${pid}: ${msg}`, 'warn');
      }
    }
  }

  return { rejected, failed };
}

export async function handleApprovalSubmit(bot, trigger, { db = null } = {}) {
  const action = trigger?.attachmentAction;
  if (!action || !action.inputs) return;

  const inputs = action.inputs;

  if (inputs.action === 'dismissApprovalCard') {
    const woId = inputs.workOrderId;
    await _removeApprovalCard(bot, action);
    if (db && woId) {
      try {
        await clearPendingApprovalCard(db, woId);
      } catch (err) {
        logger('approval:submit', `Could not clear pending card row for WO ${woId}: ${err.message}`, 'warn');
      }
    }
    try {
      await bot.say('Approval card dismissed. Approve in ServiceChannel when ready.');
    } catch (_) {}
    logger('approval:submit', `Dismissed approval card for WO ${woId || 'unknown'}`);
    return;
  }

  if (inputs.action !== 'approveNte') return;

  const woId = inputs.workOrderId;
  const proposalId = inputs.proposalId;
  const newProposalNumber = inputs.proposalNumber || proposalId;
  const newNte = inputs.newNte;
  const comment = inputs.comment || '';

  if (!woId || newNte === undefined) {
    try { await bot.say('Missing work order or NTE amount in approval submission.'); } catch (_) {}
    return;
  }

  const numericNte = Number(newNte);
  if (!Number.isFinite(numericNte) || numericNte < 0) {
    try { await bot.say('Please enter a valid non-negative NTE amount.'); } catch (_) {}
    return;
  }

  const suggestedNte = Number(inputs.suggestedNte);
  const nteWasOverridden = Number.isFinite(suggestedNte) && Math.abs(numericNte - suggestedNte) > 0.005;

  let approver = 'Webex user';
  let approverEmail = '';
  try {
    const botClientMod = await import('../integrations/webex/botClient.js');
    const details = await botClientMod.default.getPersonDetails(action.personId);
    approverEmail = details?.emails?.[0] || '';
    approver = details?.displayName || approverEmail || action.personId || 'Webex user';
  } catch (e) {
    logger('approval:submit', `Could not resolve person ${action.personId}: ${e.message}`, 'warn');
  }

  let proposalsToReject = _parseProposalsToRejectFromSubmit(inputs);
  let approveSucceeded = false;
  let nteSucceeded = false;
  const errorMessages = [];
  let rejectedList = [];

  // Step 0 — Reject superseded proposals (never reject the proposal we are about to approve)
  if (proposalsToReject.length === 0) {
    const fresh = await getProposalsToReject(woId);
    proposalsToReject = _normalizeProposalsToReject(fresh);
  }
  if (proposalId) {
    proposalsToReject = proposalsToReject.filter((p) => String(p.id) !== String(proposalId));
  }

  let rejectFailures = [];
  if (proposalsToReject.length > 0) {
    const { rejected, failed } = await _rejectSupersededProposals(
      woId, proposalsToReject, approver, approverEmail, newProposalNumber
    );
    rejectedList = rejected;
    rejectFailures = failed;
    if (failed.length > 0) {
      const failMsg = failed.map((f) => `#${f.number || f.id}: ${f.error}`).join('; ');
      logger('approval:submit', `Pre-approve reject failed for WO ${woId} (will still attempt approve): ${failMsg}`, 'warn');
    }
  }

  // Step 1 — Approve the proposal
  try {
    if (proposalId) {
      const approveBody = {
        Comments: `Approved by ${approver} via ServChan card${comment ? `: ${comment}` : ''}`,
        ProviderEmail: '',
        UserEmail: approverEmail,
        RejectReasonCodeId: 0,
        ActionSource: 'Standard',
        ReasonString: `Approved by ${approver} via ServChan${comment ? `: ${comment}` : ''}`,
      };
      await approveProposal(proposalId, approveBody);
      approveSucceeded = true;
      logger('approval:submit', `Successfully approved proposal ${proposalId} for WO ${woId} by ${approver}`);
    } else {
      errorMessages.push('Missing proposalId — cannot approve via SC API');
      logger('approval:submit', `Cannot approve WO ${woId}: no proposalId in submit payload`, 'error');
    }
  } catch (err) {
    let msg = err.message || err.toString();

    // Defensive retry: fetch fresh reject list and try once more
    if (msg.toLowerCase().includes('reject') && proposalsToReject.length === 0) {
      const fresh = _normalizeProposalsToReject(await getProposalsToReject(woId));
      if (fresh.length > 0) {
        const { rejected, failed } = await _rejectSupersededProposals(
          woId, fresh, approver, approverEmail, newProposalNumber
        );
        rejectedList = rejected;
        rejectFailures = failed;
        try {
          await approveProposal(proposalId, {
            Comments: `Approved by ${approver} via ServChan card${comment ? `: ${comment}` : ''}`,
            ProviderEmail: '',
            UserEmail: approverEmail,
            RejectReasonCodeId: 0,
            ActionSource: 'Standard',
            ReasonString: `Approved by ${approver} via ServChan${comment ? `: ${comment}` : ''}`,
          });
          approveSucceeded = true;
          logger('approval:submit', `Approved proposal ${proposalId} after defensive reject retry for WO ${woId}`);
        } catch (retryErr) {
          msg = retryErr.message || retryErr.toString();
        }
      }
    }

    if (!approveSucceeded) {
      errorMessages.push(`Proposal approval: ${msg}`);
      logger('approval:submit', `Proposal approve failed for ${proposalId}: ${msg}`, 'error');
      if (msg.includes('804') || msg.includes('no permissions')) {
        try {
          const latest = await getProposalByIdOdata(proposalId);
          const st = latest?.Status?.Primary || latest?.Status;
          if (st && String(st).toLowerCase() === 'rejected') {
            errorMessages.push(
              'The target proposal is already Rejected in ServiceChannel — it may have been rejected in the pre-approve step. Check SC for the correct pending proposal.'
            );
          }
        } catch (_) {}
      }
    }
  }

  // Step 2 — Optional NTE override
  if (approveSucceeded && nteWasOverridden) {
    try {
      await updateWorkOrderNte(woId, numericNte);
      nteSucceeded = true;
      logger('approval:submit', `NTE overridden to $${numericNte.toFixed(2)} for WO ${woId}`);
    } catch (err) {
      const msg = err.message || err.toString();
      logger('approval:submit', `Direct NTE override rejected by SC for WO ${woId}: ${msg}`, 'warn');
      errorMessages.push(`Could not force NTE to $${numericNte.toFixed(2)} — SC applied proposal amount automatically.`);
    }
  }

  // Step 3 — Confirmation
  if (approveSucceeded) {
    const nteLine = nteWasOverridden
      ? (nteSucceeded
          ? `NTE overridden to **${_formatMoney(numericNte)}**.`
          : `NTE stays at SC's auto value (${_formatMoney(suggestedNte)}) — direct override was rejected. Adjust manually in SC if needed.`)
      : `New NTE will be **${_formatMoney(suggestedNte)}** (proposal total) by ServiceChannel automatically.`;

    let rejectLine = '';
    if (rejectedList.length > 0) {
      const list = rejectedList.map((p) => `#${p.number || p.id}`).join(', ');
      rejectLine = `Prior proposal(s) rejected: **${list}**.\n\n`;
    } else if (rejectFailures.length > 0) {
      const list = rejectFailures.map((p) => `#${p.number || p.id}`).join(', ');
      rejectLine = `Note: Could not auto-reject prior proposal(s) ${list} — verify status in ServiceChannel.\n\n`;
    }

    const confirm =
      `✅ **Proposal approved** by **${approver}** for WO **${woId}**.\n\n` +
      rejectLine +
      `${nteLine}` +
      (comment ? `\n\nComment: ${comment}` : '') +
      `\n\nAn audit note has been recorded on the work order in ServiceChannel.`;

    await _removeApprovalCard(bot, action);
    if (db && woId) {
      try {
        await clearPendingApprovalCard(db, woId);
      } catch (err) {
        logger('approval:submit', `Could not clear pending card row for WO ${woId}: ${err.message}`, 'warn');
      }
    }
    await bot.say({ markdown: confirm });
    logger('approval:submit',
      `Success for WO ${woId} / proposal ${proposalId || 'n/a'}: rejected=${rejectedList.length}, ` +
      `nteOverride=${nteWasOverridden ? (nteSucceeded ? 'applied' : 'rejected') : 'not-requested'} by ${approver}`
    );
  } else {
    const isTokenError = errorMessages.some((m) => m.includes('token fetch failed'));
    const confirm = isTokenError
      ? `❌ **ServiceChannel login failed** — ServChan could not authenticate to the SC API.\n\n` +
        `This usually means \`SC_USERNAME\` / \`SC_PASSWORD\` in \`.env\` do not match the SC user, or the bot was not restarted after updating credentials.\n\n` +
        `Details: ${errorMessages.join('; ')}\n\n` +
        `Fix credentials and restart ServChan, then retry approval in ServiceChannel or re-post the card.`
      : `❌ Failed to approve proposal for WO ${woId}: ${errorMessages.join('; ')}. Please approve manually in ServiceChannel.`;
    await bot.say({ markdown: confirm });
    logger('approval:submit', `Complete failure for WO ${woId} / proposal ${proposalId || 'n/a'}: ${errorMessages.join('; ')}`, 'error');
  }
}

export default {
  buildApprovalAdaptiveCard,
  formatProposalMarkdown,
  extractLineItems,
  selectPendingProposal,
  postApprovalPackage,
  fetchAndPostApprovalCardIfNeeded,
  removeApprovalCardIfResolved,
  handleApprovalSubmit,
};
