// lib/tools/adapters/mines/catalog-import.js
//
// Adapter for mines_catalog_import.
// Reads a Disk file (XLSX), parses it with exceljs, and upserts rows into
// mines_portfolios + mines_portfolio_entries.
//
// Idempotent on (portfolio_id, name): re-running the same file produces
// 0 creates and N updates.

import path from 'node:path';
import fs from 'node:fs';
import ExcelJS from 'exceljs';
import { getDb, nowIso, newId } from '../../../db.js';
import { getDiskFile } from '../../../disk.js';
import { appendComplianceAudit } from '../../../compliance-audit.js';
import { DATA_DIR } from '../../../paths.js';

// ── Header-name to field mapping ──────────────────────────────────────────────
//
// Keys are canonical field names; values are arrays of lowercased header
// aliases (Spanish + English). Matching is case-insensitive and trim()ed.

const HEADER_MAP = {
  name: [
    'name', 'project', 'proyecto', 'nombre', 'prospect', 'target',
    'project name', 'project/prospect',
  ],
  commodity: [
    'commodity', 'commodities', 'mineral', 'metal', 'commodity/commodities',
  ],
  deposit_type: [
    'deposit', 'deposit_type', 'deposit/target type', 'tipo', 'target type',
    'deposit type', 'tipo de deposito', 'tipo de yacimiento',
  ],
  owner: [
    'owner', 'propietario', 'titular', 'company', 'empresa', 'operator',
  ],
  available_information: [
    'available information', 'informacion disponible', 'información disponible',
    'info', 'data available', 'available info', 'available data',
  ],
  has_drilling: [
    'has drilling', 'drilling', 'barrenacion', 'sondeo', 'drilled',
    'has_drilling', 'has drilling?', 'drilling?',
  ],
  deal_terms: [
    'deal terms', 'deal', 'terms', 'terminos', 'términos', 'negociacion',
    'negociación', 'deal/terms',
  ],
  data_completeness: [
    'data completeness', 'completeness', 'completitud', 'data_completeness',
    '% completeness', 'completeness %',
  ],
  notes: [
    'notes', 'notas', 'comments', 'comentarios', 'observaciones', 'note',
    'remarks', 'comentario',
  ],
};

// Build a reverse lookup: lowercased alias to field name.
const ALIAS_TO_FIELD = Object.create(null);
for (const [field, aliases] of Object.entries(HEADER_MAP)) {
  for (const alias of aliases) {
    ALIAS_TO_FIELD[alias.toLowerCase().trim()] = field;
  }
}

// ── Value parsers ─────────────────────────────────────────────────────────────

function parseDrilling(raw) {
  if (raw === null || raw === undefined || raw === '') return 0;
  const s = String(raw).trim().toLowerCase();
  if (['y', 'yes', 'si', 'sí', '1', 'true', 'x'].includes(s)) return 1;
  return 0;
}

function parseDataCompleteness(raw) {
  if (raw === null || raw === undefined || raw === '') return null;
  const s = String(raw).trim();
  // Percentage string like "80%" -> 0.8
  if (s.endsWith('%')) {
    const n = parseFloat(s.slice(0, -1));
    if (isNaN(n)) return null;
    return Math.max(0, Math.min(1, n / 100));
  }
  const n = parseFloat(s);
  if (isNaN(n)) return null;
  // In range 0-1: keep as is; in range 1-100: divide by 100.
  if (n >= 0 && n <= 1) return n;
  if (n > 1 && n <= 100) return n / 100;
  return null;
}

function cellText(cell) {
  if (cell === null || cell === undefined) return null;
  // exceljs cells expose .value; formula cells expose .result via .value.result
  const v = cell?.value ?? cell;
  if (v === null || v === undefined) return null;
  if (typeof v === 'object' && v !== null) {
    if (v.result !== undefined) return String(v.result).trim() || null;
    if (v.text !== undefined) return String(v.text).trim() || null;
    if (Array.isArray(v.richText)) return v.richText.map(r => r.text || '').join('').trim() || null;
    if (v instanceof Date) return v.toISOString();
  }
  return String(v).trim() || null;
}

// ── minesCatalogImport ────────────────────────────────────────────────────────

/**
 * @param {object} input
 * @param {string} input.file_id          Disk file ID of the XLSX.
 * @param {string} input.portfolio_name   Display name for the portfolio.
 * @param {string} [input.sheet_name]     Optional sheet name override.
 *
 * @param {object} options
 * @param {boolean} [options.owner_trusted]
 * @param {string}  [options.acting_on_behalf_of_user_id]
 * @param {string}  [options.user_id]
 *
 * @returns {Promise<object>} { status, portfolio_id, entries_created, entries_updated, entries_skipped, errors }
 */
export async function minesCatalogImport(input, options = {}) {
  const { file_id, portfolio_name, sheet_name } = input || {};
  const errors = [];

  // 1. Resolve disk file
  const file = getDiskFile(file_id);
  if (!file) {
    return { status: 'error', error: 'file_not_found', errors: ['Disk file not found: ' + file_id] };
  }

  // 2. Safety check: storage_path must be inside DATA_DIR/disk (the disk
  //    storage root). DISK_DIR is a constant computed at module-load time, but
  //    DATA_DIR is resolved from VILLA515_DATA_DIR at import time so it always
  //    reflects the correct root (including the test-isolated tmp dirs).
  //    We use realpath on both sides to resolve macOS /tmp -> /private/tmp
  //    symlinks and any other OS-level symlinks before comparing.
  //
  //    Disk files are written by uploadFile (system-controlled, not user input),
  //    so the check here is a defence-in-depth guard against a corrupt or
  //    tampered DB row — not a primary input sanitizer.
  {
    const diskRoot = path.join(DATA_DIR, 'disk');
    // storage_path MUST exist and resolve. A missing/unresolvable path is
    // treated as inaccessible rather than silently passed through path.resolve
    // (which skips symlink canonicalization and would weaken containment).
    let resolvedStoragePath;
    try {
      resolvedStoragePath = fs.realpathSync(file.storage_path);
    } catch {
      return {
        status: 'error',
        error: 'file_inaccessible',
        errors: ['Disk file path is not accessible: ' + file.storage_path],
      };
    }
    let resolvedDiskRoot;
    try { resolvedDiskRoot = fs.realpathSync(diskRoot); }
    catch { resolvedDiskRoot = path.resolve(diskRoot); }
    const rel = path.relative(resolvedDiskRoot, resolvedStoragePath);
    const isInside = rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
    if (!isInside) {
      throw new Error(
        'Unsafe file path blocked by safety policy: ' +
        'storage_path is outside allowed disk root: ' + file.storage_path
      );
    }
  }

  // 3. Parse XLSX with exceljs
  const wb = new ExcelJS.Workbook();
  await wb.xlsx.readFile(file.storage_path);

  let ws;
  if (sheet_name) {
    ws = wb.getWorksheet(sheet_name);
    if (!ws) {
      return {
        status: 'error',
        error: 'sheet_not_found',
        errors: ['Sheet "' + sheet_name + '" not found in workbook'],
      };
    }
  } else {
    ws = wb.worksheets[0];
  }

  if (!ws) {
    return { status: 'error', error: 'no_sheets', errors: ['Workbook contains no worksheets'] };
  }

  // 4. Read headers from row 1 and build colNumber -> fieldName map
  const headerRow = ws.getRow(1);
  const colFieldMap = Object.create(null);
  let hasNameCol = false;

  headerRow.eachCell({ includeEmpty: false }, (cell, colNumber) => {
    const raw = cellText(cell);
    if (!raw) return;
    const alias = raw.toLowerCase().trim();
    const field = ALIAS_TO_FIELD[alias];
    if (field) {
      colFieldMap[colNumber] = field;
      if (field === 'name') hasNameCol = true;
    }
  });

  // If the required 'name' column is entirely absent, return a graceful error.
  if (!hasNameCol) {
    return {
      status: 'error',
      error: 'missing_required_column',
      errors: ['Required column "name" (or alias: project/proyecto/nombre/prospect/target) not found in sheet headers'],
    };
  }

  // 5+6. Find/create the portfolio row AND upsert every data row inside a
  //       SINGLE transaction, so a mid-import failure cannot leave an orphaned
  //       portfolio with zero entries. Prepared statements are hoisted once and
  //       reused per row (avoids prepare-per-row overhead on large catalogs).
  const db = getDb();
  const now = nowIso();

  const findPortfolioStmt = db.prepare('SELECT id FROM mines_portfolios WHERE name = ? LIMIT 1');
  const insertPortfolioStmt = db.prepare(
    'INSERT INTO mines_portfolios (id, name, source_catalog_artifact_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
  );
  const findEntryStmt = db.prepare(
    'SELECT id FROM mines_portfolio_entries WHERE portfolio_id = ? AND name = ? LIMIT 1'
  );
  const updateEntryStmt = db.prepare(`
    UPDATE mines_portfolio_entries
    SET commodity = ?, deposit_type = ?, owner = ?, available_information = ?,
        has_drilling = ?, deal_terms = ?, data_completeness = ?, notes = ?, updated_at = ?
    WHERE id = ?
  `);
  const insertEntryStmt = db.prepare(`
    INSERT INTO mines_portfolio_entries
      (id, portfolio_id, project_id, name, commodity, deposit_type, owner,
       available_information, has_drilling, deal_terms, data_completeness, notes,
       created_at, updated_at)
    VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);

  let portfolio_id;
  let entries_created = 0;
  let entries_updated = 0;
  let entries_skipped = 0;

  db.exec('BEGIN');
  try {
    // Idempotent by portfolio name. NOTE: source_catalog_artifact_id stores the
    // Disk file id (disk_files.id), NOT an artifacts.id.
    const existing_portfolio = findPortfolioStmt.get(portfolio_name);
    if (existing_portfolio) {
      portfolio_id = existing_portfolio.id;
    } else {
      portfolio_id = newId('mp');
      insertPortfolioStmt.run(portfolio_id, portfolio_name, file_id, now, now);
    }

    ws.eachRow({ includeEmpty: false }, (row, rowNumber) => {
      if (rowNumber === 1) return; // skip header row

      const fields = Object.create(null);
      row.eachCell({ includeEmpty: false }, (cell, colNumber) => {
        const field = colFieldMap[colNumber];
        if (!field) return;
        fields[field] = cellText(cell);
      });

      const rawName = fields.name;
      if (!rawName || !rawName.trim()) {
        entries_skipped++;
        errors.push('Row ' + rowNumber + ': missing required field "name" — skipped');
        return;
      }
      const name = rawName.trim();
      const has_drilling = parseDrilling(fields.has_drilling ?? null);
      const data_completeness = parseDataCompleteness(fields.data_completeness ?? null);

      const existingEntry = findEntryStmt.get(portfolio_id, name);
      if (existingEntry) {
        updateEntryStmt.run(
          fields.commodity ?? null,
          fields.deposit_type ?? null,
          fields.owner ?? null,
          fields.available_information ?? null,
          has_drilling,
          fields.deal_terms ?? null,
          data_completeness,
          fields.notes ?? null,
          now,
          existingEntry.id
        );
        entries_updated++;
      } else {
        insertEntryStmt.run(
          newId('mpe'),
          portfolio_id,
          name,
          fields.commodity ?? null,
          fields.deposit_type ?? null,
          fields.owner ?? null,
          fields.available_information ?? null,
          has_drilling,
          fields.deal_terms ?? null,
          data_completeness,
          fields.notes ?? null,
          now,
          now
        );
        entries_created++;
      }
    });

    db.exec('COMMIT');
  } catch (err) {
    try { db.exec('ROLLBACK'); } catch {}
    throw err;
  }

  // 7. Compliance audit row (non-fatal on failure).
  try {
    appendComplianceAudit({
      user_id: options.acting_on_behalf_of_user_id || options.user_id || 'system',
      org_id: 'personal-org',
      action: 'mines.catalog.imported',
      status: 'success',
      summary:
        'Imported portfolio catalog "' + portfolio_name + '" ' +
        '— ' + entries_created + ' created, ' + entries_updated + ' updated, ' + entries_skipped + ' skipped',
      action_target_id: portfolio_id,
      action_target_type: 'portfolio',
      meta: {
        portfolio_id,
        portfolio_name,
        file_id,
        entries_created,
        entries_updated,
        entries_skipped,
        error_count: errors.length,
      },
    });
  } catch {
    // Audit failure is non-fatal.
  }

  return {
    status: 'ok',
    portfolio_id,
    entries_created,
    entries_updated,
    entries_skipped,
    errors,
  };
}
