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',
  // Do not sync Excel's pb_id into PocketBase; treat as Excel-only here
  'pb_id', 'PB_ID',
]);

// Excel-authoritative fields: always overwrite PB with Excel values
// No authoritative overwrites: we only update when canonical values differ
const EXCEL_AUTHORITATIVE_FIELDS = new Set<string>();

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 normalizeNotes(val: any): string {
  return (val ?? '').toString().replace(/\r\n/g, '\n').trim();
}

function mergeNotes(excelNotes: string | null | undefined, pbNotes: string | null | undefined): string {
  const excel = normalizeNotes(excelNotes);
  const pb = normalizeNotes(pbNotes);
  if (!excel && !pb) return '';
  if (!excel) return pb;
  if (!pb) return excel;
  if (excel === pb) return excel;
  return `${excel}\n\n${pb}`;
}

function notesMergeNeeded(excelNotes: any, pbNotes: any): boolean {
  const merged = mergeNotes(excelNotes, pbNotes);
  const pbStr = normalizeNotes(pbNotes);
  const exStr = normalizeNotes(excelNotes);
  if (exStr === pbStr) return false;
  return merged !== pbStr;
}

function canonicalizeForCompare(key: string, val: any): string {
  const lower = key.toLowerCase();
  if (key === 'Notes') return normalizeNotes(val);
  // Dates: compare as UTC ISO strings
  if (lower === 'due_date' || lower.includes('date')) {
    const iso = toUtcIso(val);
    return (iso ?? '').toString().trim();
  }
  // Booleans: compare normalized boolean as 'true'/'false'
  if (BOOLEAN_FIELDS.has(key) || lower.startsWith('is_') || lower.endsWith('_flag') || lower === 'active' || lower.includes('tax_exempt')) {
    return normalizeBoolean(val) ? 'true' : 'false';
  }
  // Generic string/number
  if (val === undefined || val === null) return '';
  return String(val).trim();
}

function recordsEqual(existing: RowObject, candidate: RowObject): boolean {
  // Compare only keys present in candidate with canonical normalization
  for (const k of Object.keys(candidate)) {
    const a = canonicalizeForCompare(k, existing?.[k]);
    const b = canonicalizeForCompare(k, candidate?.[k]);
    if (a !== b) 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
    const updatePayload = { ...record };
    if ('Notes' in updatePayload && notesMergeNeeded(updatePayload.Notes, existing.Notes)) {
      updatePayload.Notes = mergeNotes(updatePayload.Notes, existing.Notes);
    }
    // Skip update if nothing would change (canonical compare)
    if (recordsEqual(existing, updatePayload)) {
      return;
    }
    // Compute concise per-field diffs for visibility
    const changedKeys: string[] = [];
    for (const k of Object.keys(updatePayload)) {
      const before = canonicalizeForCompare(k, existing?.[k]);
      const after = canonicalizeForCompare(k, updatePayload?.[k]);
      if (before !== after) changedKeys.push(k);
    }
    await pb.collection(collection).update(existing.id, updatePayload);
    if (!silent) {
      const list = changedKeys.slice(0, 6).join(', ');
      console.log(`✓ Updated PB ${collection} id=${existing.id} (fields: ${list}${changedKeys.length > 6 ? ', …' : ''})`);
    }
  } else {
    // Create full record when not found (actual change)
    const created = await pb.collection(collection).create(record);
    if (!silent) console.log(`✓ Created PB ${collection} id=${created.id}`);
  }
}
