/**
 * Run Frappe Script / Query reports via desk API.
 * Uses CSRF + JSON body; normalizes row shape (list of lists vs list of dicts).
 */

import API_CONFIG from '../config/api';

export interface QueryReportColumn {
  fieldname: string;
  label?: string;
  fieldtype?: string;
  width?: number;
}

export interface QueryReportMessage {
  columns: QueryReportColumn[];
  result: unknown[][];
}

async function getCSRFToken(): Promise<string | null> {
  if (typeof window === 'undefined') return null;
  if ((window as unknown as { csrf_token?: string }).csrf_token) {
    return (window as unknown as { csrf_token: string }).csrf_token;
  }
  if ((window as unknown as { frappe?: { csrf_token?: string } }).frappe?.csrf_token) {
    return (window as unknown as { frappe: { csrf_token: string } }).frappe.csrf_token;
  }
  try {
    const base = API_CONFIG.BASE_URL || '';
    const res = await fetch(`${base}/api/method/frappe.sessions.get_csrf_token`, { credentials: 'include' });
    if (res.ok) {
      const json = await res.json();
      if (json.message) {
        (window as unknown as { csrf_token: string }).csrf_token = json.message;
        return json.message;
      }
    }
  } catch {
    /* ignore */
  }
  return null;
}

async function buildPostHeaders(): Promise<Record<string, string>> {
  const h: Record<string, string> = {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  };
  const csrf = await getCSRFToken();
  if (csrf) h['X-Frappe-CSRF-Token'] = csrf;
  return h;
}

function parseFrappeError(json: Record<string, unknown>, status: number): string {
  if (typeof json.message === 'string' && json.message.trim()) return json.message;
  if (json._server_messages) {
    try {
      const raw = json._server_messages;
      const arr = typeof raw === 'string' ? JSON.parse(raw) : raw;
      if (Array.isArray(arr) && arr[0]) {
        const o = typeof arr[0] === 'string' ? JSON.parse(arr[0]) : arr[0];
        if (o && typeof (o as { message?: string }).message === 'string') {
          return (o as { message: string }).message;
        }
      }
    } catch {
      /* ignore */
    }
  }
  if (typeof json.exc === 'string' && json.exc) {
    const lines = json.exc.trim().split('\n');
    return lines[lines.length - 1] || json.exc;
  }
  return `HTTP ${status}`;
}

/** Convert API result to grid rows (array of arrays) using column fieldnames. */
export function normalizeReportRows(
  columns: QueryReportColumn[],
  result: unknown,
): unknown[][] {
  if (!Array.isArray(result) || result.length === 0) return [];
  const first = result[0] as unknown;
  if (Array.isArray(first)) return result as unknown[][];
  if (first !== null && typeof first === 'object') {
    const objs = result as Record<string, unknown>[];
    const keysFromCols = (columns || []).map(c => c.fieldname).filter(Boolean);
    const keys =
      keysFromCols.length > 0 ? keysFromCols : Object.keys((objs[0] || {}) as object);
    return objs.map(obj => keys.map(k => obj[k]));
  }
  return [];
}

export async function runQueryReport(
  reportName: string,
  filters: Record<string, unknown> = {},
): Promise<QueryReportMessage> {
  const headers = await buildPostHeaders();
  const res = await fetch('/api/method/frappe.desk.query_report.run', {
    method: 'POST',
    headers,
    credentials: 'include',
    body: JSON.stringify({
      report_name: reportName,
      filters: filters ?? {},
      ignore_prepared_report: 1,
    }),
  });

  let json: Record<string, unknown>;
  try {
    json = (await res.json()) as Record<string, unknown>;
  } catch {
    throw new Error(`HTTP ${res.status}: unreadable response`);
  }

  if (!res.ok) {
    throw new Error(parseFrappeError(json, res.status));
  }
  // Some Frappe versions still return HTTP 200 with `exc` populated on validation errors.
  if (json.exc) {
    throw new Error(parseFrappeError(json, res.status));
  }

  const msg = json.message as QueryReportMessage | undefined;
  const columns = msg?.columns || [];
  const rawResult = msg?.result;
  let result = Array.isArray(rawResult) ? normalizeReportRows(columns, rawResult) : [];
  result = stripTrailingTotalRow(result);

  return { columns, result };
}

/** ERPNext query reports often append a final "Total" row. */
function stripTrailingTotalRow(rows: unknown[][]): unknown[][] {
  if (rows.length < 2) return rows;
  const firstCell = rows[rows.length - 1]?.[0];
  if (typeof firstCell === 'string' && firstCell.trim().toLowerCase() === 'total') {
    return rows.slice(0, -1);
  }
  return rows;
}

/** Build { name, value }[] for charts from report grid. */
export function reportResultToChartPoints(
  columns: QueryReportColumn[],
  result: unknown[][],
  options: { maxPoints?: number } = {},
): { name: string; value: number }[] {
  const maxPoints = options.maxPoints ?? 12;
  if (!columns?.length || !result?.length) return [];

  const objects = result.map(row => {
    const o: Record<string, unknown> = {};
    columns.forEach((c, i) => {
      o[c.fieldname] = row[i];
    });
    return o;
  });

  const valueIdx = columns.findIndex(c =>
    ['Int', 'Float', 'Currency', 'Percent', 'Duration'].includes(String(c?.fieldtype || '')),
  );

  const labelIdx = columns.findIndex(
    (c, i) =>
      i !== valueIdx &&
      (c.fieldtype === 'Link' ||
        /project|employee|task|subject|customer|item|warehouse|status|activity|date|delay/i.test(c.fieldname)),
  );
  const li = labelIdx >= 0 ? labelIdx : 0;

  if (valueIdx < 0) {
    const labelField = columns[li]?.fieldname || columns[0].fieldname;
    const tally = new Map<string, number>();
    for (const o of objects) {
      const k = truncateLabel(String(o[labelField] ?? '—'), 28);
      tally.set(k, (tally.get(k) || 0) + 1);
    }
    return Array.from(tally.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, maxPoints)
      .map(([name, value]) => ({ name, value }));
  }

  const labelField = columns[li]?.fieldname || columns[0].fieldname;
  const valueField = columns[valueIdx].fieldname;

  return objects.slice(0, maxPoints).map(o => ({
    name: truncateLabel(String(o[labelField] ?? '—'), 24),
    value: coerceNumber(o[valueField]),
  }));
}

function coerceNumber(v: unknown): number {
  if (typeof v === 'number' && !Number.isNaN(v)) return v;
  if (typeof v === 'string') {
    const n = parseFloat(v.replace(/[^0-9.-]/g, ''));
    return Number.isFinite(n) ? n : 0;
  }
  return 0;
}

function truncateLabel(s: string, max: number): string {
  const t = s.trim() || '—';
  return t.length <= max ? t : `${t.slice(0, max - 1)}…`;
}

/** Default date range for report filters (YYYY-MM-DD). */
export function reportDateRange(daysBack: number): { from_date: string; to_date: string } {
  const to = new Date();
  const from = new Date();
  from.setDate(from.getDate() - daysBack);
  const fmt = (d: Date) => d.toISOString().slice(0, 10);
  return { from_date: fmt(from), to_date: fmt(to) };
}
