const express      = require('express');
const cors         = require('cors');
const path         = require('path');
const db           = require('./db/database');
const generatePdf  = require('./pdf/generator');

const app  = express();
const PORT = process.env.PORT || 3001;

app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'client', 'dist')));

// ── Demo static route ─────────────────────────────────────────────────────────
// Serves the standalone stakeholder demo page at /demo/index.html
// Must be registered before the SPA catch-all below.
app.use('/demo', express.static(path.join(__dirname, 'demo')));

// ── Audit helper ─────────────────────────────────────────────────────────────
function audit(action, entityType, entityId, performedBy, details) {
  try {
    db.prepare(`
      INSERT INTO audit_log (action, entity_type, entity_id, performed_by, details)
      VALUES (?, ?, ?, ?, ?)
    `).run(action, entityType, entityId || null, performedBy || null,
      typeof details === 'object' ? JSON.stringify(details) : (details || null));
  } catch (e) {
    console.error('[AUDIT]', e.message);
  }
}

// ── Version info (written by Dockerfile at build time) ───────────────────────
// Falls back to { sha: 'dev' } when running outside a Docker build (local dev).
let BUILD_VERSION = { sha: 'dev', shortSha: 'dev', buildTime: null };
try {
  BUILD_VERSION = require('./client/dist/version.json');
} catch (_) { /* pre-build or local dev — stub values are fine */ }

// Health
app.get('/api/health', (req, res) => res.json({
  status:    'ok',
  timestamp: new Date().toISOString(),
  version:   BUILD_VERSION,
}));

// ── Employees ────────────────────────────────────────────────────────────────
app.get('/api/employees', (req, res) => {
  const rows = db.prepare('SELECT id, name, department, supervisor, notes FROM employees ORDER BY name ASC').all();
  res.json(rows);
});

app.post('/api/employees', (req, res) => {
  const { name, department, supervisor } = req.body;
  if (!name) return res.status(400).json({ error: 'name is required' });
  const existing = db.prepare('SELECT * FROM employees WHERE LOWER(name) = LOWER(?)').get(name);
  if (existing) {
    if (department || supervisor) {
      db.prepare('UPDATE employees SET department = COALESCE(?, department), supervisor = COALESCE(?, supervisor) WHERE id = ?')
        .run(department || null, supervisor || null, existing.id);
    }
    return res.json({ ...existing, department, supervisor });
  }
  const result = db.prepare('INSERT INTO employees (name, department, supervisor) VALUES (?, ?, ?)')
    .run(name, department || null, supervisor || null);
  audit('employee_created', 'employee', result.lastInsertRowid, null, { name });
  res.status(201).json({ id: result.lastInsertRowid, name, department, supervisor });
});

// ── Employee Edit ────────────────────────────────────────────────────────────
// PATCH /api/employees/:id — update name, department, supervisor, or notes
app.patch('/api/employees/:id', (req, res) => {
  const id  = parseInt(req.params.id);
  const emp = db.prepare('SELECT * FROM employees WHERE id = ?').get(id);
  if (!emp) return res.status(404).json({ error: 'Employee not found' });

  const { name, department, supervisor, notes, performed_by } = req.body;

  // Prevent name collision with a different employee
  if (name && name.trim() !== emp.name) {
    const clash = db.prepare('SELECT id FROM employees WHERE LOWER(name) = LOWER(?) AND id != ?').get(name.trim(), id);
    if (clash) return res.status(409).json({ error: 'An employee with that name already exists', conflictId: clash.id });
  }

  const newName       = (name       || emp.name).trim();
  const newDept       = department  !== undefined ? (department  || null) : emp.department;
  const newSupervisor = supervisor  !== undefined ? (supervisor  || null) : emp.supervisor;
  const newNotes      = notes       !== undefined ? (notes       || null) : emp.notes;

  db.prepare('UPDATE employees SET name = ?, department = ?, supervisor = ?, notes = ? WHERE id = ?')
    .run(newName, newDept, newSupervisor, newNotes, id);

  audit('employee_edited', 'employee', id, performed_by, {
    before: { name: emp.name, department: emp.department, supervisor: emp.supervisor, notes: emp.notes },
    after:  { name: newName,  department: newDept,        supervisor: newSupervisor,  notes: newNotes  },
  });

  res.json({ id, name: newName, department: newDept, supervisor: newSupervisor, notes: newNotes });
});

// ── Employee Merge ───────────────────────────────────────────────────────────
// POST /api/employees/:id/merge — reassign all violations from sourceId → id, then delete source
app.post('/api/employees/:id/merge', (req, res) => {
  const targetId = parseInt(req.params.id);
  const { source_id, performed_by } = req.body;
  if (!source_id) return res.status(400).json({ error: 'source_id is required' });

  const target = db.prepare('SELECT * FROM employees WHERE id = ?').get(targetId);
  const source = db.prepare('SELECT * FROM employees WHERE id = ?').get(source_id);
  if (!target) return res.status(404).json({ error: 'Target employee not found' });
  if (!source) return res.status(404).json({ error: 'Source employee not found' });
  if (targetId === parseInt(source_id)) return res.status(400).json({ error: 'Cannot merge an employee into themselves' });

  const mergeTransaction = db.transaction(() => {
    // Move all violations
    const moved = db.prepare('UPDATE violations SET employee_id = ? WHERE employee_id = ?').run(targetId, source_id);
    // Delete the source employee
    db.prepare('DELETE FROM employees WHERE id = ?').run(source_id);
    return moved.changes;
  });

  const violationsMoved = mergeTransaction();

  audit('employee_merged', 'employee', targetId, performed_by, {
    source: { id: source.id, name: source.name },
    target: { id: target.id, name: target.name },
    violations_reassigned: violationsMoved,
  });

  res.json({ success: true, violations_reassigned: violationsMoved });
});

// ── Employee notes (PATCH shorthand) ─────────────────────────────────────────
// PATCH /api/employees/:id/notes — save free-text notes only
app.patch('/api/employees/:id/notes', (req, res) => {
  const id  = parseInt(req.params.id);
  const emp = db.prepare('SELECT * FROM employees WHERE id = ?').get(id);
  if (!emp) return res.status(404).json({ error: 'Employee not found' });

  const { notes, performed_by } = req.body;
  const newNotes = notes !== undefined ? (notes || null) : emp.notes;

  db.prepare('UPDATE employees SET notes = ? WHERE id = ?').run(newNotes, id);
  audit('employee_notes_updated', 'employee', id, performed_by, { notes: newNotes });
  res.json({ id, notes: newNotes });
});

// Employee score (current snapshot) — includes total violations + negated count
app.get('/api/employees/:id/score', (req, res) => {
  const empId = req.params.id;

  // Active points from the 90-day rolling view
  const active = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?').get(empId);

  // Total violations (all time) and negated count
  const totals = db.prepare(`
    SELECT
      COUNT(*)                    AS total_violations,
      COALESCE(SUM(negated), 0)  AS negated_count
    FROM violations
    WHERE employee_id = ?
  `).get(empId);

  res.json({
    employee_id:      empId,
    active_points:    active ? active.active_points    : 0,
    violation_count:  active ? active.violation_count   : 0,
    total_violations: totals ? totals.total_violations  : 0,
    negated_count:    totals ? totals.negated_count     : 0,
  });
});

// ── Expiration Timeline ──────────────────────────────────────────────────────
// GET /api/employees/:id/expiration — active violations sorted by roll-off date
// Returns each active violation with days_remaining until it exits the 90-day window.
app.get('/api/employees/:id/expiration', (req, res) => {
  const rows = db.prepare(`
    SELECT
      v.id,
      v.violation_name,
      v.violation_type,
      v.category,
      v.points,
      v.incident_date,
      DATE(v.incident_date, '+90 days')                          AS expires_on,
      CAST(
        JULIANDAY(DATE(v.incident_date, '+90 days')) -
        JULIANDAY(DATE('now'))
        AS INTEGER
      )                                                          AS days_remaining
    FROM violations v
    WHERE v.employee_id = ?
      AND v.negated = 0
      AND v.incident_date >= DATE('now', '-90 days')
    ORDER BY v.incident_date ASC
  `).all(req.params.id);
  res.json(rows);
});

// Dashboard
app.get('/api/dashboard', (req, res) => {
  const rows = db.prepare(`
    SELECT e.id, e.name, e.department, e.supervisor,
           COALESCE(s.active_points, 0)  AS active_points,
           COALESCE(s.violation_count,0) AS violation_count
    FROM employees e
    LEFT JOIN active_cpas_scores s ON s.employee_id = e.id
    ORDER BY active_points DESC, e.name ASC
  `).all();
  res.json(rows);
});

// Violation history (per employee) with resolutions + amendment count
app.get('/api/violations/employee/:id', (req, res) => {
  const limit = parseInt(req.query.limit) || 50;
  const rows  = db.prepare(`
    SELECT v.*, r.resolution_type, r.details AS resolution_details,
           r.resolved_by, r.created_at AS resolved_at,
           (SELECT COUNT(*) FROM violation_amendments a WHERE a.violation_id = v.id) AS amendment_count
    FROM violations v
    LEFT JOIN violation_resolutions r ON r.violation_id = v.id
    WHERE v.employee_id = ?
    ORDER BY v.incident_date DESC, v.created_at DESC
    LIMIT ?
  `).all(req.params.id, limit);
  res.json(rows);
});

// ── Violation amendment history ──────────────────────────────────────────────
app.get('/api/violations/:id/amendments', (req, res) => {
  const rows = db.prepare(`
    SELECT * FROM violation_amendments WHERE violation_id = ? ORDER BY created_at DESC
  `).all(req.params.id);
  res.json(rows);
});

// Helper: compute prior_active_points at time of insert
function getPriorActivePoints(employeeId, incidentDate) {
  const row = db.prepare(
    `SELECT COALESCE(SUM(points),0) AS pts
     FROM violations
     WHERE employee_id = ?
       AND negated = 0
       AND incident_date >= DATE(?, '-90 days')
       AND incident_date < ?`
  ).get(employeeId, incidentDate, incidentDate);
  return row ? row.pts : 0;
}

// POST new violation
app.post('/api/violations', (req, res) => {
  const {
    employee_id, violation_type, violation_name, category,
    points, incident_date, incident_time, location,
    details, submitted_by, witness_name,
    acknowledged_by, acknowledged_date
  } = req.body;

  if (!employee_id || !violation_type || !points || !incident_date) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  const ptsInt   = parseInt(points);
  const priorPts = getPriorActivePoints(employee_id, incident_date);

  const result = db.prepare(`
    INSERT INTO violations (
      employee_id, violation_type, violation_name, category,
      points, incident_date, incident_time, location,
      details, submitted_by, witness_name,
      prior_active_points,
      acknowledged_by, acknowledged_date
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    employee_id, violation_type, violation_name || violation_type,
    category || 'General', ptsInt, incident_date,
    incident_time || null, location || null,
    details || null, submitted_by || null, witness_name || null,
    priorPts,
    acknowledged_by || null, acknowledged_date || null
  );

  audit('violation_created', 'violation', result.lastInsertRowid, submitted_by, {
    employee_id, violation_type, points: ptsInt, incident_date,
  });

  res.status(201).json({ id: result.lastInsertRowid });
});

// ── Violation Amendment (edit) ───────────────────────────────────────────────
// PATCH /api/violations/:id/amend — edit mutable fields; logs a diff per changed field
const AMENDABLE_FIELDS = ['incident_time', 'location', 'details', 'submitted_by', 'witness_name', 'acknowledged_by', 'acknowledged_date'];

app.patch('/api/violations/:id/amend', (req, res) => {
  const id        = parseInt(req.params.id);
  const { changed_by, ...updates } = req.body;

  const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(id);
  if (!violation) return res.status(404).json({ error: 'Violation not found' });
  if (violation.negated) return res.status(400).json({ error: 'Cannot amend a negated violation' });

  // Only allow whitelisted fields to be amended
  const allowed = Object.fromEntries(
    Object.entries(updates).filter(([k]) => AMENDABLE_FIELDS.includes(k))
  );
  if (Object.keys(allowed).length === 0) {
    return res.status(400).json({ error: 'No amendable fields provided', amendable: AMENDABLE_FIELDS });
  }

  const amendTransaction = db.transaction(() => {
    // Build UPDATE
    const setClauses = Object.keys(allowed).map(k => `${k} = ?`).join(', ');
    const values     = [...Object.values(allowed), id];
    db.prepare(`UPDATE violations SET ${setClauses} WHERE id = ?`).run(...values);

    // Insert an amendment record per changed field
    const insertAmendment = db.prepare(`
      INSERT INTO violation_amendments (violation_id, changed_by, field_name, old_value, new_value)
      VALUES (?, ?, ?, ?, ?)
    `);
    for (const [field, newVal] of Object.entries(allowed)) {
      const oldVal = violation[field];
      if (String(oldVal) !== String(newVal)) {
        insertAmendment.run(id, changed_by || null, field, oldVal ?? null, newVal ?? null);
      }
    }
  });

  amendTransaction();

  audit('violation_amended', 'violation', id, changed_by, { fields: Object.keys(allowed) });

  const updated = db.prepare('SELECT * FROM violations WHERE id = ?').get(id);
  res.json(updated);
});

// ── Negate a violation ───────────────────────────────────────────────────────
app.patch('/api/violations/:id/negate', (req, res) => {
  const { resolution_type, details, resolved_by } = req.body;
  const id = req.params.id;

  const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(id);
  if (!violation) return res.status(404).json({ error: 'Violation not found' });

  db.prepare('UPDATE violations SET negated = 1 WHERE id = ?').run(id);

  const existing = db.prepare('SELECT id FROM violation_resolutions WHERE violation_id = ?').get(id);
  if (existing) {
    db.prepare(`
      UPDATE violation_resolutions
      SET resolution_type = ?, details = ?, resolved_by = ?, created_at = datetime('now')
      WHERE violation_id = ?
    `).run(resolution_type || 'Resolved', details || null, resolved_by || null, id);
  } else {
    db.prepare(`
      INSERT INTO violation_resolutions (violation_id, resolution_type, details, resolved_by)
      VALUES (?, ?, ?, ?)
    `).run(id, resolution_type || 'Resolved', details || null, resolved_by || null);
  }

  audit('violation_negated', 'violation', id, resolved_by, { resolution_type });
  res.json({ success: true });
});

// ── Restore a negated violation ──────────────────────────────────────────────
app.patch('/api/violations/:id/restore', (req, res) => {
  const id = req.params.id;

  const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(id);
  if (!violation) return res.status(404).json({ error: 'Violation not found' });

  db.prepare('UPDATE violations SET negated = 0 WHERE id = ?').run(id);
  db.prepare('DELETE FROM violation_resolutions WHERE violation_id = ?').run(id);

  audit('violation_restored', 'violation', id, req.body?.performed_by, {});
  res.json({ success: true });
});

// ── Hard delete a violation ──────────────────────────────────────────────────
app.delete('/api/violations/:id', (req, res) => {
  const id = req.params.id;

  const violation = db.prepare('SELECT * FROM violations WHERE id = ?').get(id);
  if (!violation) return res.status(404).json({ error: 'Violation not found' });

  db.prepare('DELETE FROM violation_resolutions WHERE violation_id = ?').run(id);
  db.prepare('DELETE FROM violations WHERE id = ?').run(id);

  audit('violation_deleted', 'violation', id, req.body?.performed_by, {
    violation_type: violation.violation_type, employee_id: violation.employee_id,
  });
  res.json({ success: true });
});

// ── Audit log ────────────────────────────────────────────────────────────────
app.get('/api/audit', (req, res) => {
  const limit  = Math.min(parseInt(req.query.limit)  || 100, 500);
  const offset = parseInt(req.query.offset) || 0;
  const type   = req.query.entity_type;
  const id     = req.query.entity_id;

  let sql    = 'SELECT * FROM audit_log';
  const args = [];
  const where = [];
  if (type) { where.push('entity_type = ?'); args.push(type); }
  if (id)   { where.push('entity_id = ?');   args.push(id); }
  if (where.length) sql += ' WHERE ' + where.join(' AND ');
  sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
  args.push(limit, offset);

  res.json(db.prepare(sql).all(...args));
});

// ── Custom Violation Types ────────────────────────────────────────────────────
// Persisted violation type definitions stored in violation_types table.
// type_key is auto-generated (custom_<slug>) to avoid collisions with hardcoded keys.

app.get('/api/violation-types', (req, res) => {
  const rows = db.prepare('SELECT * FROM violation_types ORDER BY category ASC, name ASC').all();
  res.json(rows.map(r => ({ ...r, fields: JSON.parse(r.fields) })));
});

app.post('/api/violation-types', (req, res) => {
  const { name, category, chapter, description, min_points, max_points, fields, created_by } = req.body;
  if (!name || !name.trim()) return res.status(400).json({ error: 'name is required' });

  const minPts = parseInt(min_points) || 1;
  const maxPts = parseInt(max_points) || minPts;
  if (maxPts < minPts) return res.status(400).json({ error: 'max_points must be >= min_points' });

  // Generate a unique type_key from the name, prefixed with 'custom_'
  const base = 'custom_' + name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
  let typeKey = base;
  let suffix  = 2;
  while (db.prepare('SELECT id FROM violation_types WHERE type_key = ?').get(typeKey)) {
    typeKey = `${base}_${suffix++}`;
  }

  try {
    const result = db.prepare(`
      INSERT INTO violation_types (type_key, name, category, chapter, description, min_points, max_points, fields)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      typeKey,
      name.trim(),
      (category || 'Custom').trim(),
      chapter     || null,
      description || null,
      minPts,
      maxPts,
      JSON.stringify(fields && fields.length ? fields : ['description'])
    );
    const row = db.prepare('SELECT * FROM violation_types WHERE id = ?').get(result.lastInsertRowid);
    audit('violation_type_created', 'violation_type', result.lastInsertRowid, created_by || null, { name: row.name, category: row.category });
    res.status(201).json({ ...row, fields: JSON.parse(row.fields) });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.put('/api/violation-types/:id', (req, res) => {
  const id  = parseInt(req.params.id);
  const row = db.prepare('SELECT * FROM violation_types WHERE id = ?').get(id);
  if (!row) return res.status(404).json({ error: 'Violation type not found' });

  const { name, category, chapter, description, min_points, max_points, fields, updated_by } = req.body;
  if (!name || !name.trim()) return res.status(400).json({ error: 'name is required' });

  const minPts = parseInt(min_points) || 1;
  const maxPts = parseInt(max_points) || minPts;
  if (maxPts < minPts) return res.status(400).json({ error: 'max_points must be >= min_points' });

  db.prepare(`
    UPDATE violation_types
    SET name=?, category=?, chapter=?, description=?, min_points=?, max_points=?, fields=?, updated_at=CURRENT_TIMESTAMP
    WHERE id=?
  `).run(
    name.trim(),
    (category || 'Custom').trim(),
    chapter     || null,
    description || null,
    minPts,
    maxPts,
    JSON.stringify(fields && fields.length ? fields : ['description']),
    id
  );

  const updated = db.prepare('SELECT * FROM violation_types WHERE id = ?').get(id);
  audit('violation_type_updated', 'violation_type', id, updated_by || null, { name: updated.name, category: updated.category });
  res.json({ ...updated, fields: JSON.parse(updated.fields) });
});

app.delete('/api/violation-types/:id', (req, res) => {
  const id  = parseInt(req.params.id);
  const row = db.prepare('SELECT * FROM violation_types WHERE id = ?').get(id);
  if (!row) return res.status(404).json({ error: 'Violation type not found' });

  const usage = db.prepare('SELECT COUNT(*) as count FROM violations WHERE violation_type = ?').get(row.type_key);
  if (usage.count > 0) {
    return res.status(409).json({ error: `Cannot delete: ${usage.count} violation(s) reference this type. Negate those violations first.` });
  }

  db.prepare('DELETE FROM violation_types WHERE id = ?').run(id);
  audit('violation_type_deleted', 'violation_type', id, null, { name: row.name, type_key: row.type_key });
  res.json({ ok: true });
});

// ── PDF endpoint ─────────────────────────────────────────────────────────────
app.get('/api/violations/:id/pdf', async (req, res) => {
  try {
    const violation = db.prepare(`
      SELECT v.*, e.name as employee_name, e.department, e.supervisor
      FROM violations v
      JOIN employees e ON e.id = v.employee_id
      WHERE v.id = ?
    `).get(req.params.id);

    if (!violation) return res.status(404).json({ error: 'Violation not found' });

    const active = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?')
      .get(violation.employee_id) || { active_points: 0, violation_count: 0 };

    const scoreForPdf = {
      employee_id:     violation.employee_id,
      active_points:   violation.prior_active_points != null ? violation.prior_active_points : active.active_points,
      violation_count: active.violation_count,
    };

    const pdfBuffer = await generatePdf(violation, scoreForPdf);
    const safeName  = violation.employee_name.replace(/[^a-z0-9]/gi, '_');

    res.set({
      'Content-Type':        'application/pdf',
      'Content-Disposition': `attachment; filename="CPAS_${safeName}_${violation.incident_date}.pdf"`,
      'Content-Length':       pdfBuffer.length,
    });
    res.end(pdfBuffer);
  } catch (err) {
    console.error('[PDF]', err);
    res.status(500).json({ error: 'PDF generation failed', detail: err.message });
  }
});

// SPA fallback
app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'client', 'dist', 'index.html')));

app.listen(PORT, '0.0.0.0', () => console.log(`[CPAS] Server running on port ${PORT}`));
