import PocketBase from 'pocketbase';
import 'dotenv/config';

export type RowObject = Record<string, any>;

function requireEnv(name: string): string {
  const val = process.env[name];
  if (!val) throw new Error(`Missing required env: ${name}`);
  return val;
}

// Excel-only fields (not synced to PocketBase)
const EXCEL_ONLY_FIELDS = new Set([
  'Contact_Notes', 'Clean_Phone_Number', 'Fax', 'Phone_Number_Extn',
  'Secondary_HO', 'Primary_SL', 'POC_Name',
]);

// Excel-authoritative fields: always overwrite PB with Excel values
const EXCEL_AUTHORITATIVE_FIELDS = new Set([
  'Job_Folder_Link',
  'Flag',
  'Due_Date',
  'Job_Full_Name',
  'Job_QB_Link',
  'Voxer_Link',
  'Job_Codes',
  'Estimate_Draft',
  'Job_Status'
]);

const BOOLEAN_FIELDS = new Set(['Flag', 'Active', 'Estimate_Draft', 'Tax_Exempt']);
const SENTINEL_ASAP_DATE = '1990-01-01T00:00:00.000Z';

export function normalizeBoolean(val: any): boolean {
  if (val === true) return true;
  if (val === false) return false;
  const s = String(val).toLowerCase();
  return s === 'true' || s === '1' || s === 'yes' || s === 'y' || s === 't';
}

function excelSerialToIso(num: number): string {
  // Excel serial date: days since 1899-12-30
  const ms = (num - 25569) * 86400 * 1000;
  return new Date(ms).toISOString();
}

function toUtcIso(val: any): string {
  if (val === undefined || val === null || val === '') return '';

  if (typeof val === 'number' && !Number.isNaN(val)) {
    return excelSerialToIso(val);
  }

  if (typeof val === 'string') {
    const trimmed = val.trim();
    const numeric = parseFloat(trimmed);
    if (!Number.isNaN(numeric) && /^\d+(\.\d+)?$/.test(trimmed)) {
      return excelSerialToIso(numeric);
    }
    const d = new Date(trimmed);
    if (!Number.isNaN(d.getTime())) {
      return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds())).toISOString();
    }
    return trimmed;
  }

  return String(val);
}

export function mapRowToObject(headers: string[], row: any[]): RowObject {
  const obj: RowObject = {};
  for (let i = 0; i < headers.length; i++) {
    const key = headers[i];
    const val = row[i];
    if (!key) continue;
    
    // Skip Excel-only fields
    if (EXCEL_ONLY_FIELDS.has(key)) continue;
    
    const lower = key.toLowerCase();
    
    // Handle Due_Date with ASAP sentinel
    if (lower === 'due_date') {
      const valStr = String(val ?? '').trim();
      if (valStr.toUpperCase() === 'ASAP') {
        obj[key] = SENTINEL_ASAP_DATE;
      } else {
        obj[key] = toUtcIso(val);
      }
    }
    // Date fields (except Due_Date already handled)
    else if (lower.includes('date')) {
      obj[key] = toUtcIso(val);
    }
    // Boolean fields
    else if (BOOLEAN_FIELDS.has(key) || lower.startsWith('is_') || lower.endsWith('_flag') || lower === 'active' || lower.includes('tax_exempt')) {
      obj[key] = normalizeBoolean(val);
    }
    // All other fields
    else {
      obj[key] = val;
    }
  }
  return obj;
}

function mergeNotes(excelNotes: string | null | undefined, pbNotes: string | null | undefined): string {
  // Normalize line endings and trim
  const excel = (excelNotes ?? '').toString().replace(/\r\n/g, '\n').trim();
  const pb = (pbNotes ?? '').toString().replace(/\r\n/g, '\n').trim();
  
  if (!excel && !pb) return '';
  if (!excel) return pb;
  if (!pb) return excel;
  if (excel === pb) return excel;
  
  // Both have different content - preserve both
  return `${excel}\n\n${pb}`;
}

function notesMergeNeeded(excelNotes: any, pbNotes: any): boolean {
  // Check if a merge would change PocketBase content
  const merged = mergeNotes(excelNotes, pbNotes);
  // Normalize PB value the same way for comparison
  const pbStr = (pbNotes ?? '').toString().replace(/\r\n/g, '\n').trim();
  return merged !== pbStr;
}

function recordsEqual(existing: RowObject, candidate: RowObject): boolean {
  // Compare only keys present in candidate; treat undefined and empty string as equal empties
  for (const k of Object.keys(candidate)) {
    const a = existing?.[k];
    const b = candidate?.[k];
    const aStr = a === undefined || a === null ? '' : String(a);
    const bStr = b === undefined || b === null ? '' : String(b);
    if (aStr !== bStr) return false;
  }
  return true;
}

export async function upsertPocketBaseByKey(pb: PocketBase, collection: string, keyName: string, record: RowObject, silent: boolean = false) {
  if (keyName.toLowerCase() !== 'job_number') {
    throw new Error('Primary key must be Job_Number (exact header name)');
  }
  const keyVal = record[keyName];
  if (!keyVal) throw new Error(`Missing key ${keyName} in record`);
  const filter = `${keyName}="${String(keyVal).replace(/"/g, '\\"')}"`;
  const found = await pb.collection(collection).getList(1, 1, { filter });
  
  if (found?.items?.length) {
    const existing = found.items[0];
    
    // Handle Notes bidirectional merge - only if it would actually change
    if ('Notes' in record && notesMergeNeeded(record.Notes, existing.Notes)) {
      record.Notes = mergeNotes(record.Notes, existing.Notes);
    }
    
    // Force Excel values for authoritative fields (overwrite even if unchanged in Excel)
    const updatePayload = { ...record };
    for (const field of EXCEL_AUTHORITATIVE_FIELDS) {
      if (field in record) {
        updatePayload[field] = record[field];
      }
    }
    // Skip update if nothing would change
    if (recordsEqual(existing, updatePayload)) {
      if (!silent) console.log(`↺ No changes for PB ${collection} id=${existing.id}`);
      return;
    }
    await pb.collection(collection).update(existing.id, updatePayload);
    if (!silent) console.log(`✓ Updated PB ${collection} id=${existing.id}`);
  } else {
    const created = await pb.collection(collection).create(record);
    if (!silent) console.log(`✓ Created PB ${collection} id=${created.id}`);
  }
}
