require('dotenv').config({ path: __dirname + '/.env' });

const express = require('express');
const cors = require('cors');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const bcrypt = require('bcrypt');
const session = require('express-session');
const SqliteStore = require('connect-sqlite3')(session);
const nocodb = require('./nocodb');
const crypto = require('crypto');
const { PORT, UPLOADS_DIR, SETTINGS_PATH, DEFAULTS, QUERY_LIMITS, ALL_MODULES, TABLE_NAME_MAP, COMMENT_ENTITY_TYPES } = require('./config');
const { getRecordName, batchResolveNames, parseApproverIds, safeJsonParse, pickBodyFields, sanitizeWhereValue, getUserModules, stripSensitiveFields, getUserTeamIds, getUserVisibilityContext } = require('./helpers');
const notify = require('./notifications');

const app = express();

// ─── HELPERS ────────────────────────────────────────────────────

// Ensure uploads directory exists
const uploadsDir = UPLOADS_DIR;
if (!fs.existsSync(uploadsDir)) {
  fs.mkdirSync(uploadsDir, { recursive: true });
}

// Trust proxy when behind Nginx
if (process.env.NODE_ENV === 'production') app.set('trust proxy', 1);

// Middleware
app.use(cors({
  origin: function(origin, callback) {
    if (!origin) return callback(null, true);
    if (process.env.CORS_ORIGIN && origin !== process.env.CORS_ORIGIN) {
      return callback(new Error('Not allowed by CORS'));
    }
    callback(null, true);
  },
  credentials: true
}));
app.use(express.json());

// Session middleware
app.use(session({
  store: new SqliteStore({ db: 'sessions.db', dir: __dirname }),
  secret: (() => {
    if (process.env.SESSION_SECRET) return process.env.SESSION_SECRET;
    if (process.env.NODE_ENV === 'production') throw new Error('SESSION_SECRET is required in production');
    return 'dev-fallback-secret';
  })(),
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: DEFAULTS.sessionMaxAge
  }
}));

// Serve uploaded files
app.use('/api/uploads', express.static(uploadsDir));

// ─── APP SETTINGS (persisted to JSON) ────────────────────────────
const defaultSettings = { uploadMaxSizeMB: DEFAULTS.uploadMaxSizeMB };
function loadSettings() {
  try { return { ...defaultSettings, ...JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8')) }; }
  catch (err) { console.error('Load settings error:', err.message); return { ...defaultSettings }; }
}
function saveSettings(s) { fs.writeFileSync(SETTINGS_PATH, JSON.stringify(s, null, 2)); }
let appSettings = loadSettings();

// Multer config — dynamic size limit
const decodeOriginalName = (name) => Buffer.from(name, 'latin1').toString('utf8');
const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, uploadsDir),
  filename: (req, file, cb) => {
    file.originalname = decodeOriginalName(file.originalname);
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${path.extname(file.originalname)}`;
    cb(null, uniqueName);
  }
});
// Create upload middleware dynamically so it reads current limit
function getUpload() {
  return multer({ storage, limits: { fileSize: appSettings.uploadMaxSizeMB * 1024 * 1024 } });
}
// Wrapper that creates a fresh multer instance per request
const dynamicUpload = (fieldName) => (req, res, next) => {
  getUpload().single(fieldName)(req, res, (err) => {
    if (err) {
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(413).json({ error: `File too large. Maximum size is ${appSettings.uploadMaxSizeMB} MB.`, code: 'FILE_TOO_LARGE', maxSizeMB: appSettings.uploadMaxSizeMB });
      }
      return res.status(400).json({ error: err.message });
    }
    next();
  });
};

// ─── AUTH MIDDLEWARE ─────────────────────────────────────────────

function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  next();
}

function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.session.userId) return res.status(401).json({ error: 'Authentication required' });
    if (!roles.includes(req.session.userRole)) return res.status(403).json({ error: 'Insufficient permissions' });
    next();
  };
}

function requireOwnerOrRole(table, ...allowedRoles) {
  const nocoTable = TABLE_NAME_MAP[table];
  if (!nocoTable) throw new Error(`requireOwnerOrRole: invalid table "${table}"`);
  return async (req, res, next) => {
    if (!req.session.userId) return res.status(401).json({ error: 'Authentication required' });
    if (allowedRoles.includes(req.session.userRole)) return next();
    try {
      const row = await nocodb.get(nocoTable, req.params.id);
      if (!row) return res.status(404).json({ error: 'Not found' });
      // Check plain FK fields (Number columns)
      if (row.created_by_user_id === req.session.userId) return next();
      if (row.assigned_to_id && row.assigned_to_id === req.session.userId) return next();
      if (row.owner_id && row.owner_id === req.session.userId) return next();
      // Manager team-based access: if resource has team_id and manager is in that team
      if (req.session.userRole === 'manager' && row.team_id) {
        const myTeamIds = await getUserTeamIds(req.session.userId);
        if (myTeamIds.has(row.team_id)) return next();
      }
      return res.status(403).json({ error: 'You can only modify your own items' });
    } catch (err) {
      console.error('Owner check error:', err);
      return res.status(500).json({ error: 'Permission check failed' });
    }
  };
}

// ─── FK MIGRATION: Replace link columns with plain Number fields ──

const FK_COLUMNS = {
  Tasks: ['project_id', 'assigned_to_id', 'created_by_user_id'],
  CampaignTracks: ['campaign_id'],
  CampaignAssignments: ['campaign_id', 'member_id', 'assigner_id'],
  Projects: ['brand_id', 'owner_id', 'created_by_user_id', 'team_id'],
  Campaigns: ['brand_id', 'created_by_user_id', 'team_id'],
  Posts: ['brand_id', 'assigned_to_id', 'campaign_id', 'track_id', 'created_by_user_id'],
  Assets: ['brand_id', 'campaign_id', 'uploader_id'],
  PostAttachments: ['post_id'],
  TaskAttachments: ['task_id'],
  Comments: ['user_id'],
  BudgetEntries: ['campaign_id', 'project_id'],
  Artefacts: ['project_id', 'campaign_id'],
  PostVersions: ['post_id', 'created_by_user_id'],
  PostVersionTexts: ['version_id'],
  Issues: ['brand_id', 'assigned_to_id', 'team_id'],
  Users: ['role_id'],
  Translations: ['brand_id', 'post_id', 'created_by_user_id'],
  TranslationTexts: ['translation_id'],
};

// Maps link column names to FK field names for migration
const LINK_TO_FK = {
  Tasks: { Project: 'project_id', AssignedTo: 'assigned_to_id', CreatedByUser: 'created_by_user_id' },
  CampaignTracks: { Campaign: 'campaign_id' },
  CampaignAssignments: { Campaign: 'campaign_id', Member: 'member_id', Assigner: 'assigner_id' },
  Projects: { Brand: 'brand_id', Owner: 'owner_id', CreatedByUser: 'created_by_user_id' },
  Campaigns: { Brand: 'brand_id', CreatedByUser: 'created_by_user_id' },
  Posts: { Brand: 'brand_id', AssignedTo: 'assigned_to_id', Campaign: 'campaign_id', Track: 'track_id', CreatedByUser: 'created_by_user_id' },
  Assets: { Brand: 'brand_id', Campaign: 'campaign_id', Uploader: 'uploader_id' },
  PostAttachments: { Post: 'post_id' },
  TaskAttachments: { Task: 'task_id' },
  Comments: { User: 'user_id' },
  BudgetEntries: { Campaign: 'campaign_id', Project: 'project_id' },
};

// ─── TABLE CREATION: Ensure required tables exist ────────────────
const REQUIRED_TABLES = {
  Users: [
    { title: 'name', uidt: 'SingleLineText' },
    { title: 'email', uidt: 'Email' },
    { title: 'role', uidt: 'SingleSelect', dtxp: "'superadmin','manager','contributor'" },
    { title: 'team_role', uidt: 'SingleLineText' },
    { title: 'brands', uidt: 'LongText' },
    { title: 'phone', uidt: 'SingleLineText' },
    { title: 'avatar', uidt: 'SingleLineText' },
    { title: 'tutorial_completed', uidt: 'Checkbox' },
  ],
  Brands: [
    { title: 'name', uidt: 'SingleLineText' },
    { title: 'name_ar', uidt: 'SingleLineText' },
    { title: 'priority', uidt: 'Number' },
    { title: 'color', uidt: 'SingleLineText' },
    { title: 'icon', uidt: 'SingleLineText' },
    { title: 'category', uidt: 'SingleLineText' },
    { title: 'logo', uidt: 'SingleLineText' },
  ],
  Campaigns: [
    { title: 'name', uidt: 'SingleLineText' },
    { title: 'description', uidt: 'LongText' },
    { title: 'start_date', uidt: 'Date' },
    { title: 'end_date', uidt: 'Date' },
    { title: 'status', uidt: 'SingleSelect', dtxp: "'planning','active','paused','completed','cancelled'" },
    { title: 'color', uidt: 'SingleLineText' },
    { title: 'budget', uidt: 'Decimal' },
    { title: 'goals', uidt: 'LongText' },
    { title: 'platforms', uidt: 'LongText' },
    { title: 'budget_spent', uidt: 'Decimal' },
    { title: 'revenue', uidt: 'Decimal' },
    { title: 'impressions', uidt: 'Number' },
    { title: 'clicks', uidt: 'Number' },
    { title: 'conversions', uidt: 'Number' },
    { title: 'cost_per_click', uidt: 'Decimal' },
    { title: 'notes', uidt: 'LongText' },
    { title: 'brand_id', uidt: 'Number' },
    { title: 'created_by_user_id', uidt: 'Number' },
  ],
  CampaignTracks: [
    { title: 'name', uidt: 'SingleLineText' },
    { title: 'type', uidt: 'SingleSelect', dtxp: "'organic_social','paid_social','paid_search','email','seo','influencer','event','other'" },
    { title: 'platform', uidt: 'SingleLineText' },
    { title: 'budget_allocated', uidt: 'Decimal' },
    { title: 'budget_spent', uidt: 'Decimal' },
    { title: 'revenue', uidt: 'Decimal' },
    { title: 'impressions', uidt: 'Number' },
    { title: 'clicks', uidt: 'Number' },
    { title: 'conversions', uidt: 'Number' },
    { title: 'notes', uidt: 'LongText' },
    { title: 'status', uidt: 'SingleSelect', dtxp: "'planned','active','paused','completed'" },
    { title: 'campaign_id', uidt: 'Number' },
  ],
  CampaignAssignments: [
    { title: 'assigned_at', uidt: 'DateTime' },
    { title: 'campaign_id', uidt: 'Number' },
    { title: 'member_id', uidt: 'Number' },
    { title: 'assigner_id', uidt: 'Number' },
  ],
  Projects: [
    { title: 'name', uidt: 'SingleLineText' },
    { title: 'description', uidt: 'LongText' },
    { title: 'status', uidt: 'SingleSelect', dtxp: "'active','paused','completed','cancelled'" },
    { title: 'priority', uidt: 'SingleSelect', dtxp: "'low','medium','high','urgent'" },
    { title: 'start_date', uidt: 'Date' },
    { title: 'due_date', uidt: 'Date' },
    { title: 'brand_id', uidt: 'Number' },
    { title: 'owner_id', uidt: 'Number' },
    { title: 'created_by_user_id', uidt: 'Number' },
  ],
  Tasks: [
    { title: 'title', uidt: 'SingleLineText' },
    { title: 'description', uidt: 'LongText' },
    { title: 'status', uidt: 'SingleSelect', dtxp: "'todo','in_progress','done'" },
    { title: 'priority', uidt: 'SingleSelect', dtxp: "'low','medium','high','urgent'" },
    { title: 'start_date', uidt: 'Date' },
    { title: 'due_date', uidt: 'Date' },
    { title: 'is_personal', uidt: 'Checkbox' },
    { title: 'completed_at', uidt: 'DateTime' },
    { title: 'project_id', uidt: 'Number' },
    { title: 'assigned_to_id', uidt: 'Number' },
    { title: 'created_by_user_id', uidt: 'Number' },
  ],
  Posts: [
    { title: 'title', uidt: 'SingleLineText' },
    { title: 'description', uidt: 'LongText' },
    { title: 'status', uidt: 'SingleSelect', dtxp: "'draft','in_review','approved','scheduled','published','rejected'" },
    { title: 'platform', uidt: 'SingleLineText' },
    { title: 'platforms', uidt: 'LongText' },
    { title: 'content_type', uidt: 'SingleLineText' },
    { title: 'scheduled_date', uidt: 'DateTime' },
    { title: 'published_date', uidt: 'DateTime' },
    { title: 'notes', uidt: 'LongText' },
    { title: 'publication_links', uidt: 'LongText' },
    { title: 'brand_id', uidt: 'Number' },
    { title: 'assigned_to_id', uidt: 'Number' },
    { title: 'campaign_id', uidt: 'Number' },
    { title: 'track_id', uidt: 'Number' },
    { title: 'created_by_user_id', uidt: 'Number' },
  ],
  Assets: [
    { title: 'filename', uidt: 'SingleLineText' },
    { title: 'original_name', uidt: 'SingleLineText' },
    { title: 'mime_type', uidt: 'SingleLineText' },
    { title: 'size', uidt: 'Number' },
    { title: 'tags', uidt: 'LongText' },
    { title: 'folder', uidt: 'SingleLineText' },
    { title: 'brand_id', uidt: 'Number' },
    { title: 'campaign_id', uidt: 'Number' },
    { title: 'uploader_id', uidt: 'Number' },
  ],
  PostAttachments: [
    { title: 'filename', uidt: 'SingleLineText' },
    { title: 'original_name', uidt: 'SingleLineText' },
    { title: 'mime_type', uidt: 'SingleLineText' },
    { title: 'size', uidt: 'Number' },
    { title: 'url', uidt: 'SingleLineText' },
    { title: 'post_id', uidt: 'Number' },
  ],
  Comments: [
    { title: 'entity_type', uidt: 'SingleLineText' },
    { title: 'entity_id', uidt: 'Number' },
    { title: 'content', uidt: 'LongText' },
    { title: 'user_id', uidt: 'Number' },
  ],
  BudgetEntries: [
    { title: 'label', uidt: 'SingleLineText' },
    { title: 'amount', uidt: 'Decimal' },
    { title: 'source', uidt: 'SingleLineText' },
    { title: 'category', uidt: 'SingleLineText' },
    { title: 'date_received', uidt: 'Date' },
    { title: 'notes', uidt: 'LongText' },
    { title: 'campaign_id', uidt: 'Number' },
  ],
  TaskAttachments: [
    { title: 'filename', uidt: 'SingleLineText' },
    { title: 'original_name', uidt: 'SingleLineText' },
    { title: 'mime_type', uidt: 'SingleLineText' },
    { title: 'size', uidt: 'Number' },
    { title: 'url', uidt: 'SingleLineText' },
    { title: 'task_id', uidt: 'Number' },
  ],
  Teams: [
    { title: 'name', uidt: 'SingleLineText' },
    { title: 'description', uidt: 'LongText' },
  ],
  TeamMembers: [
    { title: 'team_id', uidt: 'Number' },
    { title: 'user_id', uidt: 'Number' },
  ],
  Artefacts: [
    { title: 'title', uidt: 'SingleLineText' },
    { title: 'description', uidt: 'LongText' },
    { title: 'type', uidt: 'SingleSelect', dtxp: "'copy','design','video','other'" },
    { title: 'status', uidt: 'SingleSelect', dtxp: "'draft','pending_review','approved','rejected','revision_requested'" },
    { title: 'content', uidt: 'LongText' },
    { title: 'feedback', uidt: 'LongText' },
    { title: 'approval_token', uidt: 'SingleLineText' },
    { title: 'token_expires_at', uidt: 'DateTime' },
    { title: 'created_by_user_id', uidt: 'Number' },
    { title: 'brand_id', uidt: 'Number' },
    { title: 'post_id', uidt: 'Number' },
    { title: 'approved_by_name', uidt: 'SingleLineText' },
    { title: 'approved_at', uidt: 'DateTime' },
    { title: 'current_version', uidt: 'Number' },
    { title: 'review_version', uidt: 'Number' },
    { title: 'project_id', uidt: 'Number' },
    { title: 'campaign_id', uidt: 'Number' },
    { title: 'approver_ids', uidt: 'SingleLineText' },
  ],
  ArtefactVersions: [
    { title: 'artefact_id', uidt: 'Number' },
    { title: 'version_number', uidt: 'Number' },
    { title: 'created_by_user_id', uidt: 'Number' },
    { title: 'created_at', uidt: 'DateTime' },
    { title: 'notes', uidt: 'LongText' },
  ],
  ArtefactVersionTexts: [
    { title: 'version_id', uidt: 'Number' },
    { title: 'language_code', uidt: 'SingleLineText' },
    { title: 'language_label', uidt: 'SingleLineText' },
    { title: 'content', uidt: 'LongText' },
  ],
  ArtefactAttachments: [
    { title: 'artefact_id', uidt: 'Number' },
    { title: 'version_id', uidt: 'Number' },
    { title: 'filename', uidt: 'SingleLineText' },
    { title: 'original_name', uidt: 'SingleLineText' },
    { title: 'mime_type', uidt: 'SingleLineText' },
    { title: 'size', uidt: 'Number' },
    { title: 'drive_url', uidt: 'SingleLineText' },
  ],
  PostVersions: [
    { title: 'post_id', uidt: 'Number' },
    { title: 'version_number', uidt: 'Number' },
    { title: 'created_by_user_id', uidt: 'Number' },
    { title: 'created_at', uidt: 'DateTime' },
    { title: 'notes', uidt: 'LongText' },
  ],
  PostVersionTexts: [
    { title: 'version_id', uidt: 'Number' },
    { title: 'language_code', uidt: 'SingleLineText' },
    { title: 'language_label', uidt: 'SingleLineText' },
    { title: 'content', uidt: 'LongText' },
  ],
  Issues: [
    { title: 'title', uidt: 'SingleLineText' },
    { title: 'description', uidt: 'LongText' },
    { title: 'type', uidt: 'SingleSelect', dtxp: "'request','correction','complaint','suggestion','other'" },
    { title: 'category', uidt: 'SingleLineText' },
    { title: 'priority', uidt: 'SingleSelect', dtxp: "'low','medium','high','urgent'" },
    { title: 'status', uidt: 'SingleSelect', dtxp: "'new','acknowledged','in_progress','resolved','declined'" },
    { title: 'submitter_name', uidt: 'SingleLineText' },
    { title: 'submitter_email', uidt: 'SingleLineText' },
    { title: 'submitter_phone', uidt: 'SingleLineText' },
    { title: 'tracking_token', uidt: 'SingleLineText' },
    { title: 'assigned_to_id', uidt: 'Number' },
    { title: 'internal_notes', uidt: 'LongText' },
    { title: 'resolution_summary', uidt: 'LongText' },
    { title: 'created_at', uidt: 'DateTime' },
    { title: 'updated_at', uidt: 'DateTime' },
    { title: 'resolved_at', uidt: 'DateTime' },
  ],
  IssueUpdates: [
    { title: 'issue_id', uidt: 'Number' },
    { title: 'message', uidt: 'LongText' },
    { title: 'is_public', uidt: 'Checkbox' },
    { title: 'author_name', uidt: 'SingleLineText' },
    { title: 'author_type', uidt: 'SingleSelect', dtxp: "'staff','submitter'" },
    { title: 'created_at', uidt: 'DateTime' },
  ],
  IssueAttachments: [
    { title: 'issue_id', uidt: 'Number' },
    { title: 'filename', uidt: 'SingleLineText' },
    { title: 'original_name', uidt: 'SingleLineText' },
    { title: 'mime_type', uidt: 'SingleLineText' },
    { title: 'size', uidt: 'Number' },
    { title: 'uploaded_by', uidt: 'SingleLineText' },
    { title: 'created_at', uidt: 'DateTime' },
  ],
  Roles: [
    { title: 'name', uidt: 'SingleLineText' },
    { title: 'color', uidt: 'SingleLineText' },
  ],
  Translations: [
    { title: 'title', uidt: 'SingleLineText' },
    { title: 'source_language', uidt: 'SingleLineText' },
    { title: 'source_content', uidt: 'LongText' },
    { title: 'status', uidt: 'SingleSelect', dtxp: "'draft','pending_review','approved','rejected','revision_requested'" },
    { title: 'brand_id', uidt: 'Number' },
    { title: 'post_id', uidt: 'Number' },
    { title: 'approver_ids', uidt: 'SingleLineText' },
    { title: 'approval_token', uidt: 'SingleLineText' },
    { title: 'token_expires_at', uidt: 'DateTime' },
    { title: 'approved_by_name', uidt: 'SingleLineText' },
    { title: 'approved_at', uidt: 'DateTime' },
    { title: 'feedback', uidt: 'LongText' },
    { title: 'created_by_user_id', uidt: 'Number' },
  ],
  TranslationTexts: [
    { title: 'translation_id', uidt: 'Number' },
    { title: 'language_code', uidt: 'SingleLineText' },
    { title: 'language_label', uidt: 'SingleLineText' },
    { title: 'content', uidt: 'LongText' },
    { title: 'option_number', uidt: 'Number' },
    { title: 'is_selected', uidt: 'Checkbox' },
  ],
};

async function ensureRequiredTables() {
  // Fetch existing tables
  const res = await fetch(`${nocodb.url}/api/v2/meta/bases/${nocodb.baseId}/tables`, {
    headers: { 'xc-token': nocodb.token },
  });
  if (!res.ok) {
    console.error('Failed to fetch tables for ensureRequiredTables');
    return;
  }
  const data = await res.json();
  const existingTables = new Set((data.list || []).map(t => t.title));

  for (const [tableName, columns] of Object.entries(REQUIRED_TABLES)) {
    if (existingTables.has(tableName)) continue;
    console.log(`  Creating table ${tableName}...`);
    try {
      const createRes = await fetch(`${nocodb.url}/api/v2/meta/bases/${nocodb.baseId}/tables`, {
        method: 'POST',
        headers: { 'xc-token': nocodb.token, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          table_name: tableName,
          title: tableName,
          columns: [
            { title: 'Id', uidt: 'ID' },
            ...columns,
          ],
        }),
      });
      if (createRes.ok) {
        console.log(`  Created table ${tableName}`);
        nocodb.clearCache(); // clear table ID cache so it picks up the new table
      } else {
        const err = await createRes.text();
        console.error(`  Failed to create table ${tableName}:`, err);
      }
    } catch (err) {
      console.error(`  Failed to create table ${tableName}:`, err.message);
    }
  }
}

// Text/string columns that must exist on tables (not FKs — those are Number type)
const TEXT_COLUMNS = {
  Projects: [{ name: 'thumbnail', uidt: 'SingleLineText' }, { name: 'color', uidt: 'SingleLineText' }],
  Tasks: [{ name: 'thumbnail', uidt: 'SingleLineText' }, { name: 'color', uidt: 'SingleLineText' }],
  Users: [
    { name: 'modules', uidt: 'LongText' },
    { name: 'password_hash', uidt: 'SingleLineText' },
    { name: 'reset_token', uidt: 'SingleLineText' },
    { name: 'reset_token_expires', uidt: 'SingleLineText' },
    { name: 'preferred_language', uidt: 'SingleLineText' },
  ],
  BudgetEntries: [{ name: 'project_id', uidt: 'Number' }, { name: 'destination', uidt: 'SingleLineText' }, { name: 'type', uidt: 'SingleLineText' }],
  Comments: [{ name: 'version_number', uidt: 'Number' }],
  Issues: [{ name: 'thumbnail', uidt: 'SingleLineText' }],
  Artefacts: [{ name: 'approver_ids', uidt: 'SingleLineText' }],
  Posts: [
    { name: 'approver_ids', uidt: 'SingleLineText' },
    { name: 'approval_token', uidt: 'SingleLineText' },
    { name: 'token_expires_at', uidt: 'SingleLineText' },
    { name: 'approved_by_name', uidt: 'SingleLineText' },
    { name: 'approved_at', uidt: 'SingleLineText' },
    { name: 'feedback', uidt: 'LongText' },
    { name: 'current_version', uidt: 'Number' },
    { name: 'review_version', uidt: 'Number' },
  ],
  PostAttachments: [{ name: 'version_id', uidt: 'Number' }],
};

async function ensureTextColumns() {
  for (const [table, columns] of Object.entries(TEXT_COLUMNS)) {
    try {
      const tableId = await nocodb.resolveTableId(table);
      const res = await fetch(`${nocodb.url}/api/v2/meta/tables/${tableId}`, {
        headers: { 'xc-token': nocodb.token },
      });
      if (!res.ok) continue;
      const meta = await res.json();
      const existingCols = new Set((meta.columns || []).map(c => c.title));

      for (const col of columns) {
        if (!existingCols.has(col.name)) {
          console.log(`  Adding text column ${table}.${col.name} (${col.uidt})...`);
          const colRes = await fetch(`${nocodb.url}/api/v2/meta/tables/${tableId}/columns`, {
            method: 'POST',
            headers: { 'xc-token': nocodb.token, 'Content-Type': 'application/json' },
            body: JSON.stringify({ title: col.name, uidt: col.uidt }),
          });
          if (colRes.ok) {
            console.log(`  ✓ Created ${table}.${col.name}`);
          } else {
            const errText = await colRes.text().catch(() => '');
            console.error(`  ✗ Failed to create ${table}.${col.name}: ${colRes.status} ${errText}`);
          }
        } else {
          console.log(`  ${table}.${col.name} already exists`);
        }
      }
    } catch (err) {
      console.error(`  Failed to ensure text columns for ${table}:`, err.message);
    }
  }
}

async function ensureFKColumns() {
  for (const [table, columns] of Object.entries(FK_COLUMNS)) {
    try {
      const tableId = await nocodb.resolveTableId(table);
      const res = await fetch(`${nocodb.url}/api/v2/meta/tables/${tableId}`, {
        headers: { 'xc-token': nocodb.token },
      });
      if (!res.ok) continue;
      const meta = await res.json();
      const existingCols = new Set((meta.columns || []).map(c => c.title));

      for (const col of columns) {
        if (!existingCols.has(col)) {
          console.log(`  Adding column ${table}.${col}...`);
          await fetch(`${nocodb.url}/api/v2/meta/tables/${tableId}/columns`, {
            method: 'POST',
            headers: { 'xc-token': nocodb.token, 'Content-Type': 'application/json' },
            body: JSON.stringify({ title: col, uidt: 'Number' }),
          });
        }
      }
    } catch (err) {
      console.error(`  Failed to ensure columns for ${table}:`, err.message);
    }
  }
}

async function backfillFKs() {
  for (const [table, linkMap] of Object.entries(LINK_TO_FK)) {
    try {
      const linkFields = Object.keys(linkMap);
      const records = await nocodb.list(table, { limit: QUERY_LIMITS.max, links: linkFields });
      let updated = 0;

      for (const record of records) {
        const patch = {};
        let needsUpdate = false;

        for (const [linkCol, fkField] of Object.entries(linkMap)) {
          const val = record?.[linkCol];
          const linkedId = Array.isArray(val) && val.length > 0 ? val[0].Id : (val && typeof val === 'object' && val.Id ? val.Id : null);
          if (linkedId && !record[fkField]) {
            patch[fkField] = linkedId;
            needsUpdate = true;
          }
        }

        if (needsUpdate) {
          await nocodb.update(table, record.Id, patch);
          updated++;
        }
      }

      if (updated > 0) console.log(`  Backfilled ${updated} records in ${table}`);
    } catch (err) {
      console.error(`  Failed to backfill ${table}:`, err.message);
    }
  }
}

// ─── HEALTH CHECK ──────────────────────────────────────────────

app.get('/api/health', async (req, res) => {
  const checks = { server: true, nocodb: false, smtp: false };
  const errors = [];

  try {
    await nocodb.resolveTableId('Users');
    checks.nocodb = true;
  } catch (err) {
    errors.push(`NocoDB: ${err.message}`);
  }

  const { getSmtpConfig } = require('./mail');
  checks.smtp = !!getSmtpConfig();
  if (!checks.smtp) errors.push('SMTP: not configured');

  const requiredEnvVars = ['NOCODB_URL', 'NOCODB_TOKEN', 'NOCODB_BASE_ID'];
  const missingEnv = requiredEnvVars.filter(v => !process.env[v]);
  if (missingEnv.length > 0) errors.push(`Missing env vars: ${missingEnv.join(', ')}`);

  const healthy = checks.server && checks.nocodb && missingEnv.length === 0;
  res.status(healthy ? 200 : 503).json({
    status: healthy ? 'healthy' : 'degraded',
    checks,
    errors: errors.length > 0 ? errors : undefined,
    timestamp: new Date().toISOString(),
  });
});

// ─── SETUP ROUTES ───────────────────────────────────────────────

app.get('/api/setup/status', async (req, res) => {
  try {
    const users = await nocodb.list('Users', { limit: 1 });
    res.json({ needsSetup: users.length === 0 });
  } catch (err) {
    res.status(500).json({ error: 'Failed to check setup status' });
  }
});

app.post('/api/setup', async (req, res) => {
  try {
    const users = await nocodb.list('Users', { limit: 1 });
    if (users.length > 0) return res.status(403).json({ error: 'Setup already completed' });
  } catch (err) {
    return res.status(500).json({ error: 'Failed to check setup status' });
  }

  const { name, email, password } = req.body;
  if (!name || !email || !password) return res.status(400).json({ error: 'Name, email, and password are required' });

  try {
    const passwordHash = await bcrypt.hash(password, 10);
    const created = await nocodb.create('Users', { name, email, role: 'superadmin', password_hash: passwordHash });
    console.log(`[SETUP] Superadmin created: ${email} (NocoDB Id: ${created.Id})`);
    res.status(201).json({ message: 'Superadmin account created. You can now log in.' });
  } catch (err) {
    console.error('Setup error:', err);
    res.status(500).json({ error: 'Failed to create superadmin account' });
  }
});

// ─── AUTH ROUTES ────────────────────────────────────────────────

app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) return res.status(400).json({ error: 'Email and password are required' });

  try {
    const users = await nocodb.list('Users', { where: `(email,eq,${sanitizeWhereValue(email)})`, limit: 1 });
    if (users.length === 0) return res.status(401).json({ error: 'Invalid email or password' });

    // nocodb.list() may not return all fields — fetch full record
    const user = await nocodb.get('Users', users[0].Id);
    if (!user || !user.password_hash) return res.status(401).json({ error: 'Invalid email or password' });

    const valid = await bcrypt.compare(password, user.password_hash);
    if (!valid) return res.status(401).json({ error: 'Invalid email or password' });

    req.session.userId = user.Id;
    req.session.userEmail = user.email;
    req.session.userRole = user.role;
    req.session.userName = user.name;

    const modules = getUserModules(user, ALL_MODULES);
    res.json({
      user: {
        id: user.Id,
        name: user.name,
        email: user.email,
        role: user.role,
        avatar: user.avatar,
        team_role: user.team_role,
        tutorial_completed: user.tutorial_completed,
        preferred_language: user.preferred_language || 'en',
        profileComplete: !!user.name,
        modules,
      },
    });
  } catch (err) {
    console.error('Login error:', err);
    res.status(500).json({ error: 'Login failed' });
  }
});

app.post('/api/auth/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.clearCookie('connect.sid');
    res.json({ success: true });
  });
});

app.post('/api/auth/forgot-password', async (req, res) => {
  const { email } = req.body;
  if (!email) return res.status(400).json({ error: 'Email is required' });

  try {
    const users = await nocodb.list('Users', { where: `(email,eq,${sanitizeWhereValue(email)})`, limit: 1 });
    if (users.length > 0) {
      const user = users[0];
      const rawToken = crypto.randomBytes(32).toString('hex');
      const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
      const expires = new Date(Date.now() + 3600000).toISOString();

      await nocodb.update('Users', user.Id, { reset_token: tokenHash, reset_token_expires: expires });

      const appUrl = process.env.APP_URL || process.env.CORS_ORIGIN || `${req.protocol}://${req.get('host')}`;
      const resetUrl = `${appUrl}/reset-password?token=${rawToken}`;

      const { sendMail } = require('./mail');
      await sendMail({
        to: email,
        subject: 'Password Reset',
        html: `<div style="font-family:sans-serif;max-width:500px;margin:0 auto">
          <h2>Password Reset</h2>
          <p>Hello ${user.name || ''},</p>
          <p>Click below to reset your password:</p>
          <p style="text-align:center;margin:30px 0">
            <a href="${resetUrl}" style="background:#3b82f6;color:white;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:bold">Reset Password</a>
          </p>
          <p style="color:#666;font-size:14px">This link expires in 1 hour. If you didn't request this, ignore this email.</p>
        </div>`,
        text: `Hello ${user.name || ''},\n\nVisit this link to reset your password:\n${resetUrl}\n\nExpires in 1 hour.`,
      });
    }
  } catch (err) {
    console.error('Forgot password error:', err);
  }
  // Always return success to prevent email enumeration
  res.json({ message: 'If an account with that email exists, a reset link has been sent.' });
});

app.post('/api/auth/reset-password', async (req, res) => {
  const { token, password } = req.body;
  if (!token || !password) return res.status(400).json({ error: 'Token and password are required' });
  if (password.length < 6) return res.status(400).json({ error: 'Password must be at least 6 characters' });

  try {
    const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
    const users = await nocodb.list('Users', { where: `(reset_token,eq,${tokenHash})`, limit: 1 });
    if (users.length === 0) return res.status(400).json({ error: 'Invalid or expired reset token' });

    // nocodb.list() may not return all fields — fetch full record
    const user = await nocodb.get('Users', users[0].Id);
    if (!user.reset_token_expires || new Date(user.reset_token_expires) < new Date()) {
      return res.status(400).json({ error: 'Invalid or expired reset token' });
    }

    const hash = await bcrypt.hash(password, 10);
    await nocodb.update('Users', user.Id, { password_hash: hash, reset_token: '', reset_token_expires: '' });
    res.json({ message: 'Password has been reset. You can now log in.' });
  } catch (err) {
    console.error('Reset password error:', err);
    res.status(500).json({ error: 'Failed to reset password' });
  }
});

app.get('/api/auth/me', requireAuth, async (req, res) => {
  try {
    const user = await nocodb.get('Users', req.session.userId);
    if (!user) return res.status(404).json({ error: 'User not found' });
    const modules = getUserModules(user, ALL_MODULES);
    res.json({
      Id: user.Id, id: user.Id, name: user.name, email: user.email,
      role: user.role, avatar: user.avatar, team_role: user.team_role,
      brands: user.brands, phone: user.phone,
      tutorial_completed: user.tutorial_completed,
      preferred_language: user.preferred_language || 'en',
      CreatedAt: user.CreatedAt, created_at: user.CreatedAt,
      profileComplete: !!user.name,
      modules,
    });
  } catch (err) {
    console.error('Auth/me error:', err);
    res.status(500).json({ error: 'Failed to fetch user' });
  }
});

app.get('/api/auth/permissions', requireAuth, (req, res) => {
  const role = req.session.userRole;
  const canManage = role === 'superadmin' || role === 'manager';
  res.json({
    role,
    canCreateCampaigns: canManage, canEditCampaigns: canManage, canDeleteCampaigns: canManage,
    canCreateProjects: canManage, canEditProjects: canManage, canDeleteProjects: canManage,
    canManageFinance: canManage, canManageTeam: canManage,
    canManageUsers: role === 'superadmin', canAssignCampaigns: canManage,
    canSetBudget: role === 'superadmin',
    canCreatePosts: true, canCreateTasks: true,
    canEditAnyPost: canManage, canDeleteAnyPost: canManage,
    canEditAnyTask: canManage, canDeleteAnyTask: canManage,
  });
});

// ─── SELF-SERVICE PROFILE ───────────────────────────────────────

app.get('/api/users/me/profile', requireAuth, async (req, res) => {
  try {
    const user = await nocodb.get('Users', req.session.userId);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json({
      Id: user.Id, id: user.Id, name: user.name, email: user.email,
      role: user.role, team_role: user.team_role, brands: user.brands,
      phone: user.phone, avatar: user.avatar, tutorial_completed: user.tutorial_completed,
    });
  } catch (err) {
    res.status(500).json({ error: 'Failed to fetch profile' });
  }
});

app.patch('/api/users/me/profile', requireAuth, async (req, res) => {
  const data = {};
  if (req.body.name !== undefined) data.name = req.body.name;
  if (req.body.phone !== undefined) data.phone = req.body.phone;

  if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });

  try {
    await nocodb.update('Users', req.session.userId, data);
    if (data.name) req.session.userName = data.name;
    const user = await nocodb.get('Users', req.session.userId);
    res.json({
      Id: user.Id, id: user.Id, name: user.name, email: user.email,
      role: user.role, team_role: user.team_role, brands: user.brands,
      phone: user.phone, avatar: user.avatar, tutorial_completed: user.tutorial_completed,
    });
  } catch (err) {
    console.error('Update profile error:', err);
    res.status(500).json({ error: 'Failed to update profile' });
  }
});

app.patch('/api/users/me/password', requireAuth, async (req, res) => {
  const { currentPassword, newPassword } = req.body;
  if (!currentPassword || !newPassword) return res.status(400).json({ error: 'Current password and new password are required' });
  if (newPassword.length < 6) return res.status(400).json({ error: 'New password must be at least 6 characters' });

  try {
    const user = await nocodb.get('Users', req.session.userId);
    if (!user || !user.password_hash) return res.status(404).json({ error: 'Credentials not found' });

    const valid = await bcrypt.compare(currentPassword, user.password_hash);
    if (!valid) return res.status(401).json({ error: 'Current password is incorrect' });

    const hash = await bcrypt.hash(newPassword, 10);
    await nocodb.update('Users', req.session.userId, { password_hash: hash });
    res.json({ message: 'Password updated successfully' });
  } catch (err) {
    console.error('Change password error:', err);
    res.status(500).json({ error: 'Failed to change password' });
  }
});

app.patch('/api/users/me/tutorial', requireAuth, async (req, res) => {
  try {
    await nocodb.update('Users', req.session.userId, { tutorial_completed: !!req.body.completed });
    res.json({ success: true, tutorial_completed: req.body.completed ? 1 : 0 });
  } catch (err) {
    res.status(500).json({ error: 'Failed to update tutorial status' });
  }
});

app.patch('/api/users/me/language', requireAuth, async (req, res) => {
  const { language } = req.body;
  if (!language || !['en', 'ar'].includes(language)) return res.status(400).json({ error: 'Invalid language' });
  try {
    await nocodb.update('Users', req.session.userId, { preferred_language: language });
    res.json({ success: true, preferred_language: language });
  } catch (err) {
    res.status(500).json({ error: 'Failed to update language preference' });
  }
});

// ─── USER MANAGEMENT ────────────────────────────────────────────

app.get('/api/users', requireAuth, async (req, res) => {
  try {
    const users = await nocodb.list('Users', { sort: 'name' });
    // Enrich with role_name
    let roles = [];
    try { roles = await nocodb.list('Roles', { limit: QUERY_LIMITS.medium }); } catch {}
    const roleMap = {};
    for (const r of roles) roleMap[r.Id] = r.name;
    res.json(stripSensitiveFields(users.map(u => ({
      ...u, id: u.Id, _id: u.Id,
      role_name: u.role_id ? (roleMap[u.role_id] || null) : null,
    }))));
  } catch (err) {
    res.status(500).json({ error: 'Failed to load users' });
  }
});

// ─── ASSIGNABLE USERS ───────────────────────────────────────────

app.get('/api/users/assignable', requireAuth, async (req, res) => {
  try {
    const users = await nocodb.list('Users', {
      sort: 'name',
    });
    res.json(stripSensitiveFields(users.map(u => ({ ...u, id: u.Id, _id: u.Id }))));
  } catch (err) {
    res.status(500).json({ error: 'Failed to load assignable users' });
  }
});

// ─── TEAM ───────────────────────────────────────────────────────

app.get('/api/users/team', requireAuth, async (req, res) => {
  try {
    const users = await nocodb.list('Users', {
      sort: 'name',
    });

    const skipBrandFilter = req.query.all === 'true' && (req.session.userRole === 'superadmin' || req.session.userRole === 'manager');
    let filtered = users;

    if (req.session.userRole !== 'superadmin' && !skipBrandFilter) {
      const currentUser = await nocodb.get('Users', req.session.userId);
      let myBrands = [];
      try { myBrands = JSON.parse(currentUser?.brands || '[]'); } catch (err) { console.error('Parse user brands:', err.message); }

      filtered = users.filter(u => {
        // Always include self, superadmins, and managers
        if (u.Id === req.session.userId || u.role === 'superadmin' || u.role === 'manager') return true;
        let theirBrands = [];
        try { theirBrands = JSON.parse(u.brands || '[]'); } catch (err) { console.error('Parse team brands:', err.message); }
        return theirBrands.some(b => myBrands.includes(b));
      });
    }

    // Attach teams + role_name to each user
    let allTeamMembers = [];
    let allTeams = [];
    let roles = [];
    try {
      allTeamMembers = await nocodb.list('TeamMembers', { limit: QUERY_LIMITS.max });
      allTeams = await nocodb.list('Teams', { limit: QUERY_LIMITS.medium });
      roles = await nocodb.list('Roles', { limit: QUERY_LIMITS.medium });
    } catch (err) { console.error('Load teams/roles for user list:', err.message); }
    const teamMap = {};
    for (const t of allTeams) teamMap[t.Id] = t.name;
    const roleMap = {};
    for (const r of roles) roleMap[r.Id] = r.name;

    res.json(stripSensitiveFields(filtered.map(u => {
      const userTeamEntries = allTeamMembers.filter(tm => tm.user_id === u.Id);
      const teams = userTeamEntries.map(tm => ({ id: tm.team_id, name: teamMap[tm.team_id] || 'Unknown' }));
      return { ...u, id: u.Id, _id: u.Id, teams, role_name: u.role_id ? (roleMap[u.role_id] || null) : null };
    })));
  } catch (err) {
    console.error('Team list error:', err);
    res.status(500).json({ error: 'Failed to load team' });
  }
});

app.post('/api/users/team', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  const { name, email, password, team_role, brands, phone, role, role_id, avatar, preferred_language } = req.body;
  if (!name) return res.status(400).json({ error: 'Name is required' });
  if (!email) return res.status(400).json({ error: 'Email is required' });

  let userRole = role || 'contributor';
  if (req.session.userRole === 'manager' && userRole !== 'contributor') {
    return res.status(403).json({ error: 'Managers can only create users with contributor permission level' });
  }
  if (userRole && !['superadmin', 'manager', 'contributor'].includes(userRole)) {
    return res.status(400).json({ error: 'Invalid permission level' });
  }

  try {
    const existing = await nocodb.list('Users', { where: `(email,eq,${sanitizeWhereValue(email)})`, limit: 1 });
    if (existing.length > 0) return res.status(409).json({ error: 'Email already exists' });

    const defaultPassword = password || 'changeme123';
    const passwordHash = await bcrypt.hash(defaultPassword, 10);
    const created = await nocodb.create('Users', {
      name, email, role: userRole, team_role: team_role || null,
      brands: JSON.stringify(brands || []), phone: phone || null,
      modules: JSON.stringify(req.body.modules || ALL_MODULES),
      password_hash: passwordHash,
      role_id: role_id || null,
      avatar: avatar || null,
      preferred_language: preferred_language || 'en',
    });

    const user = await nocodb.get('Users', created.Id);
    res.status(201).json(stripSensitiveFields({ ...user, id: user.Id, _id: user.Id }));
    notify.notifyUserInvited({ email, name, password: defaultPassword, inviterName: req.session.userName, lang: preferred_language || 'en' });
  } catch (err) {
    console.error('Create team member error:', err);
    res.status(500).json({ error: 'Failed to create team member' });
  }
});

app.patch('/api/users/team/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Users', req.params.id);
    if (!existing) return res.status(404).json({ error: 'User not found' });

    const data = {};
    for (const f of ['name', 'email', 'team_role', 'phone', 'avatar', 'preferred_language']) {
      if (req.body[f] !== undefined) data[f] = req.body[f];
    }
    if (req.body.brands !== undefined) data.brands = JSON.stringify(req.body.brands);
    if (req.body.modules !== undefined) data.modules = JSON.stringify(req.body.modules);
    if (req.body.role_id !== undefined) data.role_id = req.body.role_id;

    // Only superadmin can change permission level (role field)
    if (req.body.role !== undefined && req.session.userRole === 'superadmin') {
      if (!['superadmin', 'manager', 'contributor'].includes(req.body.role)) {
        return res.status(400).json({ error: 'Invalid permission level' });
      }
      data.role = req.body.role;
    }

    // Password change
    if (req.body.password) {
      data.password_hash = await bcrypt.hash(req.body.password, 10);
    }

    if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });

    await nocodb.update('Users', req.params.id, data);
    const user = await nocodb.get('Users', req.params.id);
    res.json(stripSensitiveFields({ ...user, id: user.Id, _id: user.Id }));
  } catch (err) {
    console.error('Update team error:', err);
    res.status(500).json({ error: 'Failed to update team member' });
  }
});

app.delete('/api/users/team/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  if (Number(req.params.id) === req.session.userId) return res.status(400).json({ error: 'Cannot delete your own account' });
  try {
    const user = await nocodb.get('Users', req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    await nocodb.delete('Users', req.params.id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete team member' });
  }
});

// ─── LEGACY TEAM API (redirects to Users) ───────────────────────

app.get('/api/team', requireAuth, async (req, res) => {
  try {
    const users = await nocodb.list('Users', { sort: 'name' });
    res.json(stripSensitiveFields(users.map(u => ({ ...u, id: u.Id, _id: u.Id }))));
  } catch (err) {
    res.status(500).json({ error: 'Failed to load team' });
  }
});

// ─── BRANDS ─────────────────────────────────────────────────────

app.get('/api/brands', requireAuth, async (req, res) => {
  try {
    const brands = await nocodb.list('Brands', { sort: 'priority,name' });
    res.json(brands);
  } catch (err) {
    res.status(500).json({ error: 'Failed to load brands' });
  }
});

app.post('/api/brands', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  const { name, name_ar, priority, color, icon } = req.body;
  if (!name) return res.status(400).json({ error: 'Name is required' });
  try {
    const created = await nocodb.create('Brands', { name, name_ar: name_ar || null, priority: priority || 2, color: color || null, icon: icon || null });
    const brand = await nocodb.get('Brands', created.Id);
    res.status(201).json(brand);
  } catch (err) {
    res.status(500).json({ error: 'Failed to create brand' });
  }
});

app.patch('/api/brands/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Brands', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Brand not found' });
    const data = {};
    for (const f of ['name', 'name_ar', 'priority', 'color', 'icon', 'logo']) {
      if (req.body[f] !== undefined) data[f] = req.body[f];
    }
    if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });
    await nocodb.update('Brands', req.params.id, data);
    const brand = await nocodb.get('Brands', req.params.id);
    res.json(brand);
  } catch (err) {
    res.status(500).json({ error: 'Failed to update brand' });
  }
});

app.post('/api/brands/:id/logo', requireAuth, requireRole('superadmin', 'manager'), dynamicUpload('file'), async (req, res) => {
  try {
    const existing = await nocodb.get('Brands', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Brand not found' });
    if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
    await nocodb.update('Brands', req.params.id, { logo: req.file.filename });
    const brand = await nocodb.get('Brands', req.params.id);
    res.json(brand);
  } catch (err) {
    res.status(500).json({ error: 'Failed to upload logo' });
  }
});

app.delete('/api/brands/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Brands', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Brand not found' });
    await nocodb.delete('Brands', req.params.id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete brand' });
  }
});

// One-time: copy Arabic names from "name" to "name_ar" for brands that have no name_ar yet
app.post('/api/brands/migrate-names', requireAuth, requireRole('superadmin'), async (req, res) => {
  try {
    const brands = await nocodb.list('Brands', { limit: QUERY_LIMITS.small });
    const updates = [];
    for (const b of brands) {
      if (!b.name_ar && b.name) {
        updates.push({ Id: b.Id, name_ar: b.name });
      }
    }
    if (updates.length > 0) {
      await nocodb.bulkUpdate('Brands', updates);
    }
    res.json({ migrated: updates.length, message: `Copied ${updates.length} brand name(s) to name_ar. Now update the "name" field with English names in NocoDB.` });
  } catch (err) {
    console.error('Brand name migration failed:', err);
    res.status(500).json({ error: 'Migration failed' });
  }
});

// ─── POSTS ──────────────────────────────────────────────────────

app.get('/api/posts/stats', requireAuth, async (req, res) => {
  try {
    const posts = await nocodb.list('Posts', { fields: 'status', limit: QUERY_LIMITS.max });
    const result = { total: posts.length };
    for (const p of posts) {
      result[p.status] = (result[p.status] || 0) + 1;
    }
    res.json(result);
  } catch (err) {
    res.status(500).json({ error: 'Failed to fetch post stats' });
  }
});

app.get('/api/posts', requireAuth, async (req, res) => {
  try {
    const { status, brand_id, assigned_to, platform, campaign_id } = req.query;
    const whereParts = [];
    if (status) whereParts.push(`(status,eq,${sanitizeWhereValue(status)})`);
    if (platform) whereParts.push(`(platform,eq,${sanitizeWhereValue(platform)})`);
    if (brand_id) whereParts.push(`(brand_id,eq,${sanitizeWhereValue(brand_id)})`);
    if (assigned_to) whereParts.push(`(assigned_to_id,eq,${sanitizeWhereValue(assigned_to)})`);
    if (campaign_id) whereParts.push(`(campaign_id,eq,${sanitizeWhereValue(campaign_id)})`);

    const where = whereParts.length > 0 ? whereParts.join('~and') : undefined;
    const posts = await nocodb.list('Posts', { where, sort: '-UpdatedAt', limit: QUERY_LIMITS.medium });

    // Team-based visibility filtering
    let filtered = posts;
    const userId = req.session.userId;
    if (req.session.userRole === 'manager') {
      const { teamCampaignIds } = await getUserVisibilityContext(userId);
      filtered = filtered.filter(p =>
        p.created_by_user_id === userId || p.assigned_to_id === userId ||
        (p.campaign_id && teamCampaignIds.has(p.campaign_id)) || !p.campaign_id
      );
    } else if (req.session.userRole === 'contributor') {
      filtered = filtered.filter(p =>
        p.created_by_user_id === userId || p.assigned_to_id === userId
      );
    }

    // Get thumbnails
    const allAttachments = await nocodb.list('PostAttachments', {
      where: "(mime_type,like,image/%)",
      limit: QUERY_LIMITS.max,
    });
    const thumbMap = {};
    for (const att of allAttachments) {
      if (att.post_id && !thumbMap[att.post_id]) thumbMap[att.post_id] = att.url;
    }

    // Collect unique IDs for name lookups
    const brandIds = new Set(), userIds = new Set(), campaignIds = new Set();
    for (const p of filtered) {
      if (p.brand_id) brandIds.add(p.brand_id);
      if (p.assigned_to_id) userIds.add(p.assigned_to_id);
      if (p.created_by_user_id) userIds.add(p.created_by_user_id);
      if (p.campaign_id) campaignIds.add(p.campaign_id);
      if (p.approver_ids) {
        for (const id of p.approver_ids.split(',').map(s => s.trim()).filter(Boolean)) {
          userIds.add(Number(id));
        }
      }
    }
    const names = await batchResolveNames({
      brand: { table: 'Brands', ids: [...brandIds] },
      user: { table: 'Users', ids: [...userIds] },
      campaign: { table: 'Campaigns', ids: [...campaignIds] },
    });

    res.json(filtered.map(p => {
      const approverIdList = p.approver_ids ? p.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
      return {
        ...p,
        brand_id: p.brand_id,
        assigned_to: p.assigned_to_id,
        campaign_id: p.campaign_id,
        created_by_user_id: p.created_by_user_id,
        brand_name: names[`brand:${p.brand_id}`] || null,
        assigned_name: names[`user:${p.assigned_to_id}`] || null,
        campaign_name: names[`campaign:${p.campaign_id}`] || null,
        creator_user_name: names[`user:${p.created_by_user_id}`] || null,
        thumbnail_url: thumbMap[p.Id] || null,
        approvers: approverIdList.map(id => ({ id: Number(id), name: names[`user:${Number(id)}`] || null })),
      };
    }));
  } catch (err) {
    console.error('GET /posts error:', err);
    res.status(500).json({ error: 'Failed to load posts' });
  }
});

app.post('/api/posts', requireAuth, async (req, res) => {
  const { title, description, brand_id, assigned_to, status, platform, platforms, content_type, scheduled_date, notes, campaign_id, approver_ids } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });

  const platformsArr = platforms || (platform ? [platform] : []);
  try {
    const created = await nocodb.create('Posts', {
      title, description: description || null,
      status: status || 'draft',
      platform: platformsArr[0] || null,
      platforms: JSON.stringify(platformsArr),
      content_type: content_type || null,
      scheduled_date: scheduled_date || null,
      notes: notes || null,
      publication_links: '[]',
      brand_id: brand_id ? Number(brand_id) : null,
      assigned_to_id: assigned_to ? Number(assigned_to) : null,
      campaign_id: campaign_id ? Number(campaign_id) : null,
      approver_ids: approver_ids || null,
      created_by_user_id: req.session.userId,
    });

    const post = await nocodb.get('Posts', created.Id);
    const approverIdList = post.approver_ids ? post.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
    const approverNames = {};
    for (const id of approverIdList) {
      approverNames[id] = await getRecordName('Users', Number(id));
    }
    res.status(201).json({
      ...post,
      assigned_to: post.assigned_to_id,
      brand_name: await getRecordName('Brands', post.brand_id),
      assigned_name: await getRecordName('Users', post.assigned_to_id),
      campaign_name: await getRecordName('Campaigns', post.campaign_id),
      creator_user_name: await getRecordName('Users', post.created_by_user_id),
      approvers: approverIdList.map(id => ({ id: Number(id), name: approverNames[id] || null })),
    });
  } catch (err) {
    console.error('Create post error:', err);
    res.status(500).json({ error: 'Failed to create post' });
  }
});

// Bulk delete posts
app.post('/api/posts/bulk-delete', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const { ids } = req.body;
    if (!Array.isArray(ids) || ids.length === 0) return res.status(400).json({ error: 'ids array required' });
    // Delete related PostAttachments
    for (const id of ids) {
      const atts = await nocodb.list('PostAttachments', { where: `(post_id,eq,${sanitizeWhereValue(id)})`, limit: QUERY_LIMITS.large });
      if (atts.length > 0) await nocodb.bulkDelete('PostAttachments', atts.map(a => ({ Id: a.Id })));
    }
    await nocodb.bulkDelete('Posts', ids.map(id => ({ Id: id })));
    res.json({ deleted: ids.length });
  } catch (err) {
    console.error('Bulk delete posts error:', err);
    res.status(500).json({ error: 'Failed to bulk delete posts' });
  }
});

app.patch('/api/posts/:id', requireAuth, requireOwnerOrRole('posts', 'superadmin', 'manager'), async (req, res) => {
  const { id } = req.params;
  try {
    const existing = await nocodb.get('Posts', id);
    if (!existing) return res.status(404).json({ error: 'Post not found' });

    const data = {};
    for (const f of ['title', 'description', 'status', 'platform', 'content_type', 'scheduled_date', 'published_date', 'notes']) {
      if (req.body[f] !== undefined) data[f] = req.body[f];
    }
    if (req.body.platforms !== undefined) {
      data.platforms = JSON.stringify(req.body.platforms);
      if (!req.body.platform) data.platform = req.body.platforms[0] || null;
    }
    if (req.body.publication_links !== undefined) {
      data.publication_links = JSON.stringify(req.body.publication_links);
    }
    if (req.body.brand_id !== undefined) data.brand_id = req.body.brand_id ? Number(req.body.brand_id) : null;
    if (req.body.assigned_to !== undefined) data.assigned_to_id = req.body.assigned_to ? Number(req.body.assigned_to) : null;
    if (req.body.campaign_id !== undefined) data.campaign_id = req.body.campaign_id ? Number(req.body.campaign_id) : null;
    if (req.body.approver_ids !== undefined) data.approver_ids = req.body.approver_ids || null;

    // Approval gate: can't skip to approved/scheduled/published without review
    if (['approved', 'scheduled', 'published'].includes(req.body.status) && existing.status !== 'approved' && req.body.status !== existing.status) {
      if (req.body.status === 'approved' && existing.status !== 'in_review') {
        // Only public review can set approved — unless superadmin
        if (req.session.userRole !== 'superadmin') {
          return res.status(400).json({ error: 'Post must be approved through the review process' });
        }
      }
      if (['scheduled', 'published'].includes(req.body.status) && existing.status !== 'approved' && existing.status !== 'scheduled') {
        if (req.session.userRole !== 'superadmin') {
          return res.status(400).json({ error: 'Post must be approved before it can be scheduled or published' });
        }
      }
    }

    // Publish validation
    if (req.body.status === 'published') {
      let currentPlatforms, currentLinks;
      currentPlatforms = req.body.platforms || safeJsonParse(existing.platforms, []);
      currentLinks = req.body.publication_links || safeJsonParse(existing.publication_links, []);
      const missing = currentPlatforms.filter(pl => {
        const link = currentLinks.find(l => l.platform === pl);
        return !link || !link.url || !link.url.trim();
      });
      if (missing.length > 0) {
        return res.status(400).json({ error: `Cannot publish: missing publication links for: ${missing.join(', ')}`, missingPlatforms: missing });
      }
      if (!req.body.published_date) data.published_date = new Date().toISOString();
    }

    await nocodb.update('Posts', id, data);

    const post = await nocodb.get('Posts', id);
    const approverIdList = post.approver_ids ? post.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
    const approverNames = {};
    for (const aid of approverIdList) {
      approverNames[aid] = await getRecordName('Users', Number(aid));
    }
    res.json({
      ...post,
      assigned_to: post.assigned_to_id,
      brand_name: await getRecordName('Brands', post.brand_id),
      assigned_name: await getRecordName('Users', post.assigned_to_id),
      campaign_name: await getRecordName('Campaigns', post.campaign_id),
      creator_user_name: await getRecordName('Users', post.created_by_user_id),
      approvers: approverIdList.map(id => ({ id: Number(id), name: approverNames[id] || null })),
    });
  } catch (err) {
    console.error('Update post error:', err);
    res.status(500).json({ error: 'Failed to update post' });
  }
});

app.delete('/api/posts/:id', requireAuth, requireOwnerOrRole('posts', 'superadmin', 'manager'), async (req, res) => {
  try {
    await nocodb.delete('Posts', req.params.id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete post' });
  }
});

// ─── POST ATTACHMENTS ───────────────────────────────────────────

app.get('/api/posts/:id/attachments', requireAuth, async (req, res) => {
  try {
    const attachments = await nocodb.list('PostAttachments', {
      where: `(post_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: '-CreatedAt', limit: QUERY_LIMITS.large,
    });
    res.json(attachments);
  } catch (err) {
    res.status(500).json({ error: 'Failed to load attachments' });
  }
});

app.post('/api/posts/:id/attachments', requireAuth, dynamicUpload('file'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });

  try {
    const post = await nocodb.get('Posts', req.params.id);
    if (!post) return res.status(404).json({ error: 'Post not found' });

    // Contributor check
    if (req.session.userRole === 'contributor') {
      if (post.created_by_user_id !== req.session.userId && post.assigned_to_id !== req.session.userId) {
        fs.unlinkSync(path.join(uploadsDir, req.file.filename));
        return res.status(403).json({ error: 'You can only manage attachments on your own posts' });
      }
    }

    const url = `/api/uploads/${req.file.filename}`;
    const created = await nocodb.create('PostAttachments', {
      filename: req.file.filename,
      original_name: req.file.originalname,
      mime_type: req.file.mimetype,
      size: req.file.size,
      url,
      post_id: Number(req.params.id),
    });

    const attachment = await nocodb.get('PostAttachments', created.Id);
    res.status(201).json(attachment);
  } catch (err) {
    console.error('Upload attachment error:', err);
    res.status(500).json({ error: 'Failed to upload attachment' });
  }
});

app.post('/api/posts/:id/attachments/from-asset', requireAuth, async (req, res) => {
  const { asset_id } = req.body;
  if (!asset_id) return res.status(400).json({ error: 'asset_id is required' });

  try {
    const post = await nocodb.get('Posts', req.params.id);
    if (!post) return res.status(404).json({ error: 'Post not found' });

    if (req.session.userRole === 'contributor') {
      if (post.created_by_user_id !== req.session.userId && post.assigned_to_id !== req.session.userId) {
        return res.status(403).json({ error: 'You can only manage attachments on your own posts' });
      }
    }

    const asset = await nocodb.get('Assets', asset_id);
    if (!asset) return res.status(404).json({ error: 'Asset not found' });

    const url = `/api/uploads/${asset.filename}`;
    const created = await nocodb.create('PostAttachments', {
      filename: asset.filename,
      original_name: asset.original_name,
      mime_type: asset.mime_type,
      size: asset.size,
      url,
      post_id: Number(req.params.id),
    });

    const attachment = await nocodb.get('PostAttachments', created.Id);
    res.status(201).json(attachment);
  } catch (err) {
    res.status(500).json({ error: 'Failed to create attachment from asset' });
  }
});

app.delete('/api/attachments/:id', requireAuth, async (req, res) => {
  try {
    const attachment = await nocodb.get('PostAttachments', req.params.id);
    if (!attachment) return res.status(404).json({ error: 'Attachment not found' });

    // Contributor check: get linked post
    if (req.session.userRole === 'contributor') {
      if (attachment.post_id) {
        const post = await nocodb.get('Posts', attachment.post_id);
        if (post.created_by_user_id !== req.session.userId && post.assigned_to_id !== req.session.userId) {
          return res.status(403).json({ error: 'You can only manage attachments on your own posts' });
        }
      }
    }

    // Check if file is referenced elsewhere before deleting
    const allAttachments = await nocodb.list('PostAttachments', {
      where: `(filename,eq,${attachment.filename})`,
      limit: 10,
    });
    const allAssets = await nocodb.list('Assets', {
      where: `(filename,eq,${attachment.filename})`,
      limit: 10,
    });
    if (allAttachments.length <= 1 && allAssets.length === 0) {
      const filePath = path.join(uploadsDir, attachment.filename);
      if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
    }

    await nocodb.delete('PostAttachments', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete attachment error:', err);
    res.status(500).json({ error: 'Failed to delete attachment' });
  }
});

// ─── POST REVIEW / APPROVAL ─────────────────────────────────────

// Submit post for review — generates approval token
app.post('/api/posts/:id/submit-review', requireAuth, requireOwnerOrRole('posts', 'superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Posts', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Post not found' });

    const token = require('crypto').randomUUID();
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + DEFAULTS.tokenExpiryDays);

    const updateData = {
      approval_token: token,
      token_expires_at: expiresAt.toISOString(),
    };
    // Only change status to in_review for draft/rejected posts
    if (!existing.status || existing.status === 'draft' || existing.status === 'rejected') {
      updateData.status = 'in_review';
      updateData.approved_by_name = null;
      updateData.approved_at = null;
      updateData.feedback = null;
    }
    // Track which version is under review
    if (existing.current_version) {
      updateData.review_version = existing.current_version;
    }
    await nocodb.update('Posts', req.params.id, updateData);

    const reviewUrl = `${req.protocol}://${req.get('host')}/review-post/${token}`;
    res.json({ success: true, token, reviewUrl, expiresAt: expiresAt.toISOString() });
    notify.notifyReviewSubmitted({ type: 'post', record: { ...existing, ...updateData }, reviewUrl });
  } catch (err) {
    console.error('Submit post review error:', err);
    res.status(500).json({ error: 'Failed to submit for review' });
  }
});

// Public: Get post for review (no auth)
app.get('/api/public/review-post/:token', async (req, res) => {
  try {
    const posts = await nocodb.list('Posts', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (posts.length === 0) return res.status(404).json({ error: 'Review link not found or expired' });

    const post = posts[0];

    if (post.token_expires_at && new Date(post.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    // Get attachments
    const attachments = await nocodb.list('PostAttachments', {
      where: `(post_id,eq,${post.Id})`,
      limit: QUERY_LIMITS.large,
    });

    // Resolve approver names
    const approvers = [];
    if (post.approver_ids) {
      for (const id of post.approver_ids.split(',').map(s => s.trim()).filter(Boolean)) {
        approvers.push({ id: Number(id), name: await getRecordName('Users', Number(id)) });
      }
    }

    // Get comments
    let comments = [];
    try {
      comments = await nocodb.list('Comments', {
        where: `(entity_type,eq,post)~and(entity_id,eq,${post.Id})`,
        sort: 'CreatedAt',
        limit: QUERY_LIMITS.large,
      });
    } catch {}

    res.json({
      ...post,
      platforms: safeJsonParse(post.platforms, []),
      publication_links: safeJsonParse(post.publication_links, []),
      brand_name: await getRecordName('Brands', post.brand_id),
      assigned_name: await getRecordName('Users', post.assigned_to_id),
      creator_name: await getRecordName('Users', post.created_by_user_id),
      approvers,
      attachments: attachments.map(a => ({
        ...a,
        url: a.url || `/api/uploads/${a.filename}`,
      })),
      comments,
    });
  } catch (err) {
    console.error('Public post review fetch error:', err);
    res.status(500).json({ error: 'Failed to load post for review' });
  }
});

// Public: Approve post
app.post('/api/public/review-post/:token/approve', async (req, res) => {
  const { approved_by_name, feedback } = req.body;
  try {
    const posts = await nocodb.list('Posts', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (posts.length === 0) return res.status(404).json({ error: 'Review link not found' });
    const post = posts[0];

    if (post.token_expires_at && new Date(post.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    await nocodb.update('Posts', post.Id, {
      status: 'approved',
      approved_by_name: approved_by_name || 'Anonymous',
      approved_at: new Date().toISOString(),
      feedback: feedback || null,
    });

    res.json({ success: true, message: 'Post approved successfully' });
    notify.notifyApproved({ type: 'post', record: post, approverName: approved_by_name });
  } catch (err) {
    console.error('Post approve error:', err);
    res.status(500).json({ error: 'Failed to approve post' });
  }
});

// Public: Reject post
app.post('/api/public/review-post/:token/reject', async (req, res) => {
  const { approved_by_name, feedback } = req.body;
  if (!feedback || !feedback.trim()) {
    return res.status(400).json({ error: 'Feedback is required when rejecting' });
  }
  try {
    const posts = await nocodb.list('Posts', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (posts.length === 0) return res.status(404).json({ error: 'Review link not found' });
    const post = posts[0];

    if (post.token_expires_at && new Date(post.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    await nocodb.update('Posts', post.Id, {
      status: 'rejected',
      approved_by_name: approved_by_name || 'Anonymous',
      feedback: feedback || '',
    });

    res.json({ success: true, message: 'Post rejected' });
    notify.notifyRejected({ type: 'post', record: post, approverName: approved_by_name, feedback });
  } catch (err) {
    console.error('Post reject error:', err);
    res.status(500).json({ error: 'Failed to reject post' });
  }
});

// ─── POST VERSIONS ──────────────────────────────────────────────

// List all versions for a post
app.get('/api/posts/:id/versions', requireAuth, async (req, res) => {
  try {
    const versions = await nocodb.list('PostVersions', {
      where: `(post_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: 'version_number',
      limit: QUERY_LIMITS.large,
    });
    const enriched = [];
    for (const v of versions) {
      const creatorName = await getRecordName('Users', v.created_by_user_id);
      enriched.push({ ...v, creator_name: creatorName });
    }
    res.json(enriched);
  } catch (err) {
    console.error('List post versions error:', err);
    res.status(500).json({ error: 'Failed to load versions' });
  }
});

// Create new version
app.post('/api/posts/:id/versions', requireAuth, async (req, res) => {
  const { notes, copy_from_previous } = req.body;
  try {
    const post = await nocodb.get('Posts', req.params.id);
    if (!post) return res.status(404).json({ error: 'Post not found' });

    if (req.session.userRole === 'contributor' && post.created_by_user_id !== req.session.userId && post.assigned_to_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only create versions for your own posts' });
    }

    const versions = await nocodb.list('PostVersions', {
      where: `(post_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: '-version_number',
      limit: 1,
    });
    const newVersionNumber = versions.length > 0 ? versions[0].version_number + 1 : 1;

    const created = await nocodb.create('PostVersions', {
      post_id: Number(req.params.id),
      version_number: newVersionNumber,
      created_by_user_id: req.session.userId,
      created_at: new Date().toISOString(),
      notes: notes || `Version ${newVersionNumber}`,
    });

    await nocodb.update('Posts', req.params.id, { current_version: newVersionNumber });

    // Copy texts from previous version if requested
    if (copy_from_previous && versions.length > 0) {
      const prevVersionId = versions[0].Id;
      const prevTexts = await nocodb.list('PostVersionTexts', {
        where: `(version_id,eq,${prevVersionId})`,
        limit: QUERY_LIMITS.large,
      });
      for (const text of prevTexts) {
        await nocodb.create('PostVersionTexts', {
          version_id: created.Id,
          language_code: text.language_code,
          language_label: text.language_label,
          content: text.content,
        });
      }
    }

    const version = await nocodb.get('PostVersions', created.Id);
    const creatorName = await getRecordName('Users', version.created_by_user_id);
    res.status(201).json({ ...version, creator_name: creatorName });
  } catch (err) {
    console.error('Create post version error:', err);
    res.status(500).json({ error: 'Failed to create version' });
  }
});

// Get specific version with texts and attachments
app.get('/api/posts/:id/versions/:versionId', requireAuth, async (req, res) => {
  try {
    const version = await nocodb.get('PostVersions', req.params.versionId);
    if (!version) return res.status(404).json({ error: 'Version not found' });
    if (version.post_id !== Number(req.params.id)) {
      return res.status(400).json({ error: 'Version does not belong to this post' });
    }

    const [texts, attachments] = await Promise.all([
      nocodb.list('PostVersionTexts', {
        where: `(version_id,eq,${sanitizeWhereValue(req.params.versionId)})`,
        limit: QUERY_LIMITS.large,
      }),
      nocodb.list('PostAttachments', {
        where: `(version_id,eq,${sanitizeWhereValue(req.params.versionId)})`,
        limit: QUERY_LIMITS.large,
      }),
    ]);

    const creatorName = await getRecordName('Users', version.created_by_user_id);

    res.json({
      ...version,
      creator_name: creatorName,
      texts,
      attachments: attachments.map(a => ({
        ...a,
        url: a.url || `/api/uploads/${a.filename}`,
      })),
    });
  } catch (err) {
    console.error('Get post version error:', err);
    res.status(500).json({ error: 'Failed to load version' });
  }
});

// Add/update language text for a version
app.post('/api/posts/:id/versions/:versionId/texts', requireAuth, async (req, res) => {
  const { language_code, language_label, content } = req.body;
  if (!language_code || !language_label || !content) {
    return res.status(400).json({ error: 'language_code, language_label, and content are required' });
  }

  try {
    const post = await nocodb.get('Posts', req.params.id);
    if (!post) return res.status(404).json({ error: 'Post not found' });

    if (req.session.userRole === 'contributor' && post.created_by_user_id !== req.session.userId && post.assigned_to_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only manage texts for your own posts' });
    }

    const existing = await nocodb.list('PostVersionTexts', {
      where: `(version_id,eq,${sanitizeWhereValue(req.params.versionId)})~and(language_code,eq,${sanitizeWhereValue(language_code)})`,
      limit: 1,
    });

    let text;
    if (existing.length > 0) {
      await nocodb.update('PostVersionTexts', existing[0].Id, { language_label, content });
      text = await nocodb.get('PostVersionTexts', existing[0].Id);
    } else {
      const created = await nocodb.create('PostVersionTexts', {
        version_id: Number(req.params.versionId),
        language_code,
        language_label,
        content,
      });
      text = await nocodb.get('PostVersionTexts', created.Id);
    }

    res.json(text);
  } catch (err) {
    console.error('Add/update post text error:', err);
    res.status(500).json({ error: 'Failed to add/update text' });
  }
});

// Delete language text
app.delete('/api/post-version-texts/:id', requireAuth, async (req, res) => {
  try {
    const text = await nocodb.get('PostVersionTexts', req.params.id);
    if (!text) return res.status(404).json({ error: 'Text not found' });

    const version = await nocodb.get('PostVersions', text.version_id);
    const post = await nocodb.get('Posts', version.post_id);

    if (req.session.userRole === 'contributor' && post.created_by_user_id !== req.session.userId && post.assigned_to_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only manage texts for your own posts' });
    }

    await nocodb.delete('PostVersionTexts', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete post text error:', err);
    res.status(500).json({ error: 'Failed to delete text' });
  }
});

// Upload attachment to specific version
app.post('/api/posts/:id/versions/:versionId/attachments', requireAuth, dynamicUpload('file'), async (req, res) => {
  try {
    const post = await nocodb.get('Posts', req.params.id);
    if (!post) return res.status(404).json({ error: 'Post not found' });

    if (req.session.userRole === 'contributor' && post.created_by_user_id !== req.session.userId && post.assigned_to_id !== req.session.userId) {
      if (req.file) fs.unlinkSync(path.join(uploadsDir, req.file.filename));
      return res.status(403).json({ error: 'You can only manage attachments on your own posts' });
    }

    if (!req.file) {
      return res.status(400).json({ error: 'File upload is required' });
    }

    const url = `/api/uploads/${req.file.filename}`;
    const created = await nocodb.create('PostAttachments', {
      filename: req.file.filename,
      original_name: req.file.originalname,
      mime_type: req.file.mimetype,
      size: req.file.size,
      url,
      post_id: Number(req.params.id),
      version_id: Number(req.params.versionId),
    });

    const attachment = await nocodb.get('PostAttachments', created.Id);
    res.status(201).json(attachment);
  } catch (err) {
    console.error('Upload post version attachment error:', err);
    res.status(500).json({ error: 'Failed to upload attachment' });
  }
});

// ─── ASSETS ─────────────────────────────────────────────────────

app.get('/api/assets', requireAuth, async (req, res) => {
  try {
    const { folder, tags } = req.query;
    const whereParts = [];
    if (folder) whereParts.push(`(folder,eq,${sanitizeWhereValue(folder)})`);
    if (req.query.brand_id) whereParts.push(`(brand_id,eq,${sanitizeWhereValue(req.query.brand_id)})`);
    if (req.query.campaign_id) whereParts.push(`(campaign_id,eq,${sanitizeWhereValue(req.query.campaign_id)})`);
    const where = whereParts.length > 0 ? whereParts.join('~and') : undefined;

    let assets = await nocodb.list('Assets', { where, sort: '-CreatedAt', limit: QUERY_LIMITS.large });

    if (tags) {
      assets = assets.filter(a => (a.tags || '').includes(tags));
    }

    const brandIds = new Set(), userIds = new Set(), campaignIds = new Set();
    for (const a of assets) {
      if (a.brand_id) brandIds.add(a.brand_id);
      if (a.campaign_id) campaignIds.add(a.campaign_id);
      if (a.uploader_id) userIds.add(a.uploader_id);
    }
    const names = await batchResolveNames({
      brand: { table: 'Brands', ids: [...brandIds] },
      campaign: { table: 'Campaigns', ids: [...campaignIds] },
      user: { table: 'Users', ids: [...userIds] },
    });

    res.json(assets.map(a => ({
      ...a,
      brand_name: names[`brand:${a.brand_id}`] || null,
      campaign_name: names[`campaign:${a.campaign_id}`] || null,
      uploader_name: names[`user:${a.uploader_id}`] || null,
    })));
  } catch (err) {
    res.status(500).json({ error: 'Failed to load assets' });
  }
});

app.post('/api/assets/upload', requireAuth, dynamicUpload('file'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });

  try {
    const { brand_id, campaign_id, uploaded_by, folder, tags } = req.body;
    const created = await nocodb.create('Assets', {
      filename: req.file.filename,
      original_name: req.file.originalname,
      mime_type: req.file.mimetype,
      size: req.file.size,
      tags: tags || '[]',
      folder: folder || 'general',
      brand_id: brand_id ? Number(brand_id) : null,
      campaign_id: campaign_id ? Number(campaign_id) : null,
      uploader_id: uploaded_by ? Number(uploaded_by) : null,
    });

    const asset = await nocodb.get('Assets', created.Id);
    res.status(201).json({
      ...asset,
      brand_name: await getRecordName('Brands', asset.brand_id),
      campaign_name: await getRecordName('Campaigns', asset.campaign_id),
      uploader_name: await getRecordName('Users', asset.uploader_id),
    });
  } catch (err) {
    console.error('Upload asset error:', err);
    res.status(500).json({ error: 'Failed to upload asset' });
  }
});

// Bulk delete assets
app.post('/api/assets/bulk-delete', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const { ids } = req.body;
    if (!Array.isArray(ids) || ids.length === 0) return res.status(400).json({ error: 'ids array required' });
    for (const id of ids) {
      const asset = await nocodb.get('Assets', id);
      if (asset && asset.filename) {
        const refs = await nocodb.list('PostAttachments', { where: `(filename,eq,${asset.filename})`, limit: 1 });
        if (refs.length === 0) {
          const filePath = path.join(uploadsDir, asset.filename);
          if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
        }
      }
    }
    await nocodb.bulkDelete('Assets', ids.map(id => ({ Id: id })));
    res.json({ deleted: ids.length });
  } catch (err) {
    console.error('Bulk delete assets error:', err);
    res.status(500).json({ error: 'Failed to bulk delete assets' });
  }
});

app.delete('/api/assets/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const asset = await nocodb.get('Assets', req.params.id);
    if (!asset) return res.status(404).json({ error: 'Asset not found' });

    const refs = await nocodb.list('PostAttachments', {
      where: `(filename,eq,${asset.filename})`,
      limit: 1,
    });
    if (refs.length === 0) {
      const filePath = path.join(uploadsDir, asset.filename);
      if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
    }

    await nocodb.delete('Assets', req.params.id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete asset' });
  }
});

// ─── CAMPAIGNS ──────────────────────────────────────────────────

app.get('/api/campaigns', requireAuth, async (req, res) => {
  try {
    const { status } = req.query;
    const whereParts = [];
    if (status) whereParts.push(`(status,eq,${sanitizeWhereValue(status)})`);
    const where = whereParts.length > 0 ? whereParts.join('~and') : undefined;

    let campaigns = await nocodb.list('Campaigns', { where, sort: '-start_date', limit: QUERY_LIMITS.medium });

    // Filter by brand
    if (req.query.brand_id) {
      campaigns = campaigns.filter(c => c.brand_id === Number(req.query.brand_id));
    }

    // Team-based visibility scoping
    const userId = req.session.userId;
    if (req.session.userRole === 'manager') {
      const myTeamIds = await getUserTeamIds(userId);
      const myCampaignIds = await getUserCampaignIds(userId);
      campaigns = campaigns.filter(c =>
        c.created_by_user_id === userId || myCampaignIds.has(c.Id) ||
        (c.team_id && myTeamIds.has(c.team_id)) || !c.team_id
      );
    } else if (req.session.userRole === 'contributor') {
      const myCampaignIds = await getUserCampaignIds(userId);
      campaigns = campaigns.filter(c =>
        c.created_by_user_id === userId || myCampaignIds.has(c.Id)
      );
    }

    // Enrich with names
    const brandIds = new Set(), userIds = new Set(), teamIds = new Set();
    for (const c of campaigns) {
      if (c.brand_id) brandIds.add(c.brand_id);
      if (c.created_by_user_id) userIds.add(c.created_by_user_id);
      if (c.team_id) teamIds.add(c.team_id);
    }
    const names = await batchResolveNames({
      brand: { table: 'Brands', ids: [...brandIds] },
      user: { table: 'Users', ids: [...userIds] },
      team: { table: 'Teams', ids: [...teamIds] },
    });

    res.json(campaigns.map(c => ({
      ...c,
      brand_name: names[`brand:${c.brand_id}`] || null,
      creator_user_name: names[`user:${c.created_by_user_id}`] || null,
      team_name: names[`team:${c.team_id}`] || null,
    })));
  } catch (err) {
    console.error('GET /campaigns error:', err);
    res.status(500).json({ error: 'Failed to load campaigns' });
  }
});

app.get('/api/campaigns/:id', requireAuth, async (req, res) => {
  try {
    const campaign = await nocodb.get('Campaigns', req.params.id);
    if (!campaign) return res.status(404).json({ error: 'Campaign not found' });

    // Access check
    if (req.session.userRole !== 'superadmin') {
      const userId = req.session.userId;
      if (campaign.created_by_user_id !== userId) {
        const assignments = await nocodb.list('CampaignAssignments', { limit: QUERY_LIMITS.max });
        const hasAccess = assignments.some(a => a.campaign_id === campaign.Id && a.member_id === userId);
        if (!hasAccess) return res.status(403).json({ error: 'You do not have access to this campaign' });
      }
    }

    const brandName = await getRecordName('Brands', campaign.brand_id);
    const teamName = await getRecordName('Teams', campaign.team_id);
    res.json({ ...campaign, brand_name: brandName, team_name: teamName });
  } catch (err) {
    res.status(500).json({ error: 'Failed to load campaign' });
  }
});

app.post('/api/campaigns', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  const { name, description, brand_id, start_date, end_date, status, color, budget, goals, platforms, team_id } = req.body;
  if (!name) return res.status(400).json({ error: 'Name is required' });
  if (!start_date || !end_date) return res.status(400).json({ error: 'Start and end dates are required' });

  const effectiveBudget = req.session.userRole === 'superadmin' ? (budget || null) : null;

  try {
    const created = await nocodb.create('Campaigns', {
      name, description: description || null,
      start_date, end_date,
      status: status || 'planning',
      color: color || null,
      budget: effectiveBudget,
      goals: goals || null,
      platforms: JSON.stringify(platforms || []),
      budget_spent: 0, revenue: 0, impressions: 0, clicks: 0, conversions: 0, cost_per_click: 0,
      notes: '',
      brand_id: brand_id ? Number(brand_id) : null,
      team_id: team_id ? Number(team_id) : null,
      created_by_user_id: req.session.userId,
    });

    // Auto-assign creator
    await nocodb.create('CampaignAssignments', {
      assigned_at: new Date().toISOString(),
      campaign_id: created.Id,
      member_id: req.session.userId,
      assigner_id: req.session.userId,
    });

    const campaign = await nocodb.get('Campaigns', created.Id);
    res.status(201).json({
      ...campaign,
      brand_name: await getRecordName('Brands', campaign.brand_id),
      team_name: await getRecordName('Teams', campaign.team_id),
    });
    notify.notifyCampaignCreated({ campaign, creatorUserId: req.session.userId });
  } catch (err) {
    console.error('Create campaign error:', err);
    res.status(500).json({ error: 'Failed to create campaign' });
  }
});

app.patch('/api/campaigns/:id', requireAuth, requireOwnerOrRole('campaigns', 'superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Campaigns', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Campaign not found' });

    const body = { ...req.body };
    if (req.session.userRole !== 'superadmin') delete body.budget;

    const data = {};
    for (const f of ['name', 'description', 'start_date', 'end_date', 'status', 'color', 'budget', 'goals',
      'budget_spent', 'revenue', 'impressions', 'clicks', 'conversions', 'cost_per_click', 'notes']) {
      if (body[f] !== undefined) data[f] = body[f];
    }
    if (body.platforms !== undefined) data.platforms = JSON.stringify(body.platforms);
    if (body.brand_id !== undefined) data.brand_id = body.brand_id ? Number(body.brand_id) : null;
    if (body.team_id !== undefined) data.team_id = body.team_id ? Number(body.team_id) : null;

    if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });

    await nocodb.update('Campaigns', req.params.id, data);

    const campaign = await nocodb.get('Campaigns', req.params.id);
    res.json({
      ...campaign,
      brand_name: await getRecordName('Brands', campaign.brand_id),
      team_name: await getRecordName('Teams', campaign.team_id),
    });
  } catch (err) {
    console.error('Update campaign error:', err);
    res.status(500).json({ error: 'Failed to update campaign' });
  }
});

app.delete('/api/campaigns/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  const id = Number(req.params.id);
  try {
    // Delete related posts
    const posts = await nocodb.list('Posts', { where: `(campaign_id,eq,${id})`, limit: QUERY_LIMITS.max });
    for (const p of posts) await nocodb.delete('Posts', p.Id);

    // Delete campaign tracks
    const tracks = await nocodb.list('CampaignTracks', { where: `(campaign_id,eq,${id})`, limit: QUERY_LIMITS.max });
    for (const t of tracks) await nocodb.delete('CampaignTracks', t.Id);

    // Delete campaign assignments
    const assignments = await nocodb.list('CampaignAssignments', { where: `(campaign_id,eq,${id})`, limit: QUERY_LIMITS.max });
    for (const a of assignments) await nocodb.delete('CampaignAssignments', a.Id);

    // Unlink assets (clear campaign_id)
    const assets = await nocodb.list('Assets', { where: `(campaign_id,eq,${id})`, limit: QUERY_LIMITS.max });
    for (const a of assets) await nocodb.update('Assets', a.Id, { campaign_id: null });

    await nocodb.delete('Campaigns', id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete campaign error:', err);
    res.status(500).json({ error: 'Failed to delete campaign' });
  }
});

// ─── CAMPAIGN ASSIGNMENTS ───────────────────────────────────────

app.get('/api/campaigns/:id/assignments', requireAuth, async (req, res) => {
  try {
    const filtered = await nocodb.list('CampaignAssignments', {
      where: `(campaign_id,eq,${sanitizeWhereValue(req.params.id)})`,
      limit: QUERY_LIMITS.max,
    });

    // Enrich with user data (member_id is a plain Number field, not a link)
    const enriched = [];
    for (const a of filtered) {
      const memberId = a.member_id;
      const assignerId = a.assigner_id;
      let memberData = {}, assignerName = null;
      if (memberId) {
        try {
          const u = await nocodb.get('Users', memberId);
          memberData = { user_name: u.name, user_email: u.email, user_avatar: u.avatar, user_role: u.role, user_id: u.Id };
        } catch (err) { console.error('Resolve assignment member:', err.message); }
      }
      if (assignerId) {
        try {
          const u = await nocodb.get('Users', assignerId);
          assignerName = u.name;
        } catch (err) { console.error('Resolve assignment assigner:', err.message); }
      }
      enriched.push({ ...a, ...memberData, assigned_by_name: assignerName });
    }
    res.json(enriched);
  } catch (err) {
    res.status(500).json({ error: 'Failed to load assignments' });
  }
});

app.post('/api/campaigns/:id/assignments', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  const { user_ids } = req.body;
  if (!Array.isArray(user_ids) || user_ids.length === 0) return res.status(400).json({ error: 'user_ids array is required' });

  try {
    const campaign = await nocodb.get('Campaigns', req.params.id);
    if (!campaign) return res.status(404).json({ error: 'Campaign not found' });

    if (req.session.userRole !== 'superadmin' && campaign.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'Only the campaign creator or superadmin can assign members' });
    }

    // Check existing assignments to avoid duplicates
    const existingAssignments = await nocodb.list('CampaignAssignments', {
      where: `(campaign_id,eq,${sanitizeWhereValue(req.params.id)})`,
      limit: QUERY_LIMITS.max,
    });
    const existing = new Set(existingAssignments.map(a => a.member_id));

    for (const userId of user_ids) {
      if (existing.has(userId)) continue;
      await nocodb.create('CampaignAssignments', {
        assigned_at: new Date().toISOString(),
        member_id: userId,
        assigner_id: req.session.userId,
        campaign_id: Number(req.params.id),
      });
    }

    // Return updated list
    const filtered = await nocodb.list('CampaignAssignments', {
      where: `(campaign_id,eq,${sanitizeWhereValue(req.params.id)})`,
      limit: QUERY_LIMITS.max,
    });
    const enriched = [];
    for (const a of filtered) {
      if (a.member_id) {
        try {
          const u = await nocodb.get('Users', a.member_id);
          enriched.push({ ...a, user_name: u.name, user_email: u.email, user_avatar: u.avatar, user_role: u.role, user_id: u.Id });
        } catch (err) { console.error('Resolve assignment member:', err.message); enriched.push(a); }
      } else {
        enriched.push(a);
      }
    }
    res.json(enriched);
  } catch (err) {
    console.error('Create assignment error:', err);
    res.status(500).json({ error: 'Failed to create assignment' });
  }
});

app.delete('/api/campaigns/:id/assignments/:userId', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const campaign = await nocodb.get('Campaigns', req.params.id);
    if (!campaign) return res.status(404).json({ error: 'Campaign not found' });

    if (req.session.userRole !== 'superadmin' && campaign.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'Only the campaign creator or superadmin can remove members' });
    }

    const assignments = await nocodb.list('CampaignAssignments', {
      where: `(campaign_id,eq,${sanitizeWhereValue(req.params.id)})~and(member_id,eq,${sanitizeWhereValue(req.params.userId)})`,
      limit: 1,
    });
    if (assignments.length === 0) return res.status(404).json({ error: 'Assignment not found' });

    await nocodb.delete('CampaignAssignments', assignments[0].Id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete assignment' });
  }
});

// ─── BUDGET ENTRIES ─────────────────────────────────────────────

app.get('/api/budget', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    let entries = await nocodb.list('BudgetEntries', { sort: '-date_received', limit: QUERY_LIMITS.max });

    if (req.session.userRole !== 'superadmin') {
      const userId = req.session.userId;
      const myCampaignIds = await getUserCampaignIds(userId);
      entries = entries.filter(e => !e.campaign_id || myCampaignIds.has(e.campaign_id));
    }

    const campaignIds = new Set();
    const projectIds = new Set();
    for (const e of entries) {
      if (e.campaign_id) campaignIds.add(e.campaign_id);
      if (e.project_id) projectIds.add(e.project_id);
    }
    const names = await batchResolveNames({
      campaign: { table: 'Campaigns', ids: [...campaignIds] },
      project: { table: 'Projects', ids: [...projectIds] },
    });

    res.json(entries.map(e => ({
      ...e,
      campaign_name: names[`campaign:${e.campaign_id}`] || null,
      project_name: names[`project:${e.project_id}`] || null,
    })));
  } catch (err) {
    res.status(500).json({ error: 'Failed to load budget entries' });
  }
});

app.post('/api/budget', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  const { label, amount, source, destination, campaign_id, project_id, category, date_received, notes, type } = req.body;
  if (!label || !amount || !date_received) return res.status(400).json({ error: 'Label, amount, and date are required' });

  try {
    const created = await nocodb.create('BudgetEntries', {
      label, amount, source: source || null, destination: destination || null,
      category: category || 'marketing', date_received, notes: notes || '',
      campaign_id: campaign_id ? Number(campaign_id) : null,
      project_id: project_id ? Number(project_id) : null,
      type: type || 'income',
    });
    const entry = await nocodb.get('BudgetEntries', created.Id);
    res.status(201).json({
      ...entry,
      campaign_name: await getRecordName('Campaigns', entry.campaign_id),
      project_name: await getRecordName('Projects', entry.project_id),
    });
  } catch (err) {
    res.status(500).json({ error: 'Failed to create budget entry' });
  }
});

app.patch('/api/budget/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('BudgetEntries', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Budget entry not found' });

    const data = {};
    for (const f of ['label', 'amount', 'source', 'destination', 'category', 'date_received', 'notes', 'type']) {
      if (req.body[f] !== undefined) data[f] = req.body[f];
    }
    if (req.body.campaign_id !== undefined) data.campaign_id = req.body.campaign_id ? Number(req.body.campaign_id) : null;
    if (req.body.project_id !== undefined) data.project_id = req.body.project_id ? Number(req.body.project_id) : null;

    if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });

    await nocodb.update('BudgetEntries', req.params.id, data);

    const entry = await nocodb.get('BudgetEntries', req.params.id);
    res.json({
      ...entry,
      campaign_name: await getRecordName('Campaigns', entry.campaign_id),
      project_name: await getRecordName('Projects', entry.project_id),
    });
  } catch (err) {
    res.status(500).json({ error: 'Failed to update budget entry' });
  }
});

app.delete('/api/budget/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('BudgetEntries', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Entry not found' });
    await nocodb.delete('BudgetEntries', req.params.id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete budget entry' });
  }
});

// Finance summary
app.get('/api/finance/summary', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const isSuperadmin = req.session.userRole === 'superadmin';
    const userId = req.session.userId;

    let campaigns = await nocodb.list('Campaigns', { limit: QUERY_LIMITS.max });
    let budgetEntries = await nocodb.list('BudgetEntries', { limit: QUERY_LIMITS.max });

    if (!isSuperadmin) {
      const myCampaignIds = await getUserCampaignIds(userId);
      campaigns = campaigns.filter(c => myCampaignIds.has(c.Id));
      budgetEntries = budgetEntries.filter(e => !e.campaign_id || myCampaignIds.has(e.campaign_id));
    }

    const incomeEntries = budgetEntries.filter(e => (e.type || 'income') === 'income');
    const expenseEntries = budgetEntries.filter(e => e.type === 'expense');
    const totalIncome = isSuperadmin
      ? incomeEntries.reduce((sum, e) => sum + (e.amount || 0), 0)
      : campaigns.reduce((sum, c) => sum + (c.budget || 0), 0);
    const totalExpenses = expenseEntries.reduce((sum, e) => sum + (e.amount || 0), 0);
    const totalReceived = totalIncome;

    const allTracks = await nocodb.list('CampaignTracks', { limit: QUERY_LIMITS.max });
    const campaignStats = campaigns.map(c => {
      const cTracks = allTracks.filter(t => t.campaign_id === c.Id);
      const cEntries = incomeEntries.filter(e => e.campaign_id && Number(e.campaign_id) === c.Id);
      const cExpenses = expenseEntries.filter(e => e.campaign_id && Number(e.campaign_id) === c.Id);
      return {
        id: c.Id, name: c.name, budget: c.budget, status: c.status,
        budget_from_entries: cEntries.reduce((s, e) => s + (e.amount || 0), 0),
        expenses: cExpenses.reduce((s, e) => s + (e.amount || 0), 0),
        tracks_allocated: cTracks.reduce((s, t) => s + (t.budget_allocated || 0), 0),
        tracks_spent: cTracks.reduce((s, t) => s + (t.budget_spent || 0), 0),
        tracks_revenue: cTracks.reduce((s, t) => s + (t.revenue || 0), 0),
        tracks_impressions: cTracks.reduce((s, t) => s + (t.impressions || 0), 0),
        tracks_clicks: cTracks.reduce((s, t) => s + (t.clicks || 0), 0),
        tracks_conversions: cTracks.reduce((s, t) => s + (t.conversions || 0), 0),
      };
    });

    const totals = campaignStats.reduce((acc, c) => ({
      allocated: acc.allocated + c.tracks_allocated,
      spent: acc.spent + c.tracks_spent,
      revenue: acc.revenue + c.tracks_revenue,
      impressions: acc.impressions + c.tracks_impressions,
      clicks: acc.clicks + c.tracks_clicks,
      conversions: acc.conversions + c.tracks_conversions,
    }), { allocated: 0, spent: 0, revenue: 0, impressions: 0, clicks: 0, conversions: 0 });

    const totalCampaignBudget = campaignStats.reduce((s, c) => s + (c.budget || 0), 0);

    // Project budget breakdown
    let projects = await nocodb.list('Projects', { limit: QUERY_LIMITS.max });
    if (!isSuperadmin) {
      projects = projects.filter(p => p.owner_id === userId || p.created_by_user_id === userId);
    }
    const projectStats = projects.map(p => {
      const pEntries = incomeEntries.filter(e => e.project_id && Number(e.project_id) === p.Id);
      const pExpenses = expenseEntries.filter(e => e.project_id && Number(e.project_id) === p.Id);
      return {
        id: p.Id, name: p.name, status: p.status,
        budget_allocated: pEntries.reduce((s, e) => s + (e.amount || 0), 0),
        expenses: pExpenses.reduce((s, e) => s + (e.amount || 0), 0),
      };
    });
    const totalProjectBudget = projectStats.reduce((s, p) => s + p.budget_allocated, 0);

    const unallocated = totalReceived - totalCampaignBudget - totalProjectBudget;

    res.json({
      totalReceived, ...totals, totalExpenses,
      remaining: totalReceived - totalCampaignBudget - totalProjectBudget - totals.spent - totalExpenses,
      roi: totals.spent > 0 ? ((totals.revenue - totals.spent) / totals.spent * 100) : 0,
      campaigns: campaignStats,
      projects: projectStats,
      totalCampaignBudget,
      totalProjectBudget,
      unallocated,
    });
  } catch (err) {
    console.error('Finance summary error:', err);
    res.status(500).json({ error: 'Failed to load finance summary' });
  }
});

// ─── CAMPAIGN TRACKS ────────────────────────────────────────────

app.get('/api/campaigns/:id/tracks', requireAuth, async (req, res) => {
  try {
    const tracks = await nocodb.list('CampaignTracks', {
      where: `(campaign_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: 'CreatedAt', limit: QUERY_LIMITS.large,
    });
    res.json(tracks);
  } catch (err) {
    res.status(500).json({ error: 'Failed to load tracks' });
  }
});

app.post('/api/campaigns/:id/tracks', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const campaign = await nocodb.get('Campaigns', req.params.id);
    if (!campaign) return res.status(404).json({ error: 'Campaign not found' });

    const { name, type, platform, budget_allocated, status, notes } = req.body;
    const created = await nocodb.create('CampaignTracks', {
      name: name || null, type: type || 'organic_social',
      platform: platform || null, budget_allocated: budget_allocated || 0,
      status: status || 'planned', notes: notes || '',
      budget_spent: 0, revenue: 0, impressions: 0, clicks: 0, conversions: 0,
      campaign_id: Number(req.params.id),
    });

    const track = await nocodb.get('CampaignTracks', created.Id);
    res.status(201).json(track);
  } catch (err) {
    res.status(500).json({ error: 'Failed to create track' });
  }
});

app.patch('/api/tracks/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('CampaignTracks', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Track not found' });

    const data = {};
    for (const f of ['name', 'type', 'platform', 'budget_allocated', 'budget_spent', 'revenue', 'impressions', 'clicks', 'conversions', 'notes', 'status']) {
      if (req.body[f] !== undefined) data[f] = req.body[f];
    }
    if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });

    await nocodb.update('CampaignTracks', req.params.id, data);
    const track = await nocodb.get('CampaignTracks', req.params.id);
    res.json(track);
  } catch (err) {
    res.status(500).json({ error: 'Failed to update track' });
  }
});

app.delete('/api/tracks/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('CampaignTracks', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Track not found' });
    await nocodb.delete('CampaignTracks', req.params.id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete track' });
  }
});

// Campaign posts
app.get('/api/campaigns/:id/posts', requireAuth, async (req, res) => {
  try {
    const filtered = await nocodb.list('Posts', {
      where: `(campaign_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: '-CreatedAt', limit: QUERY_LIMITS.max,
    });

    const allAttachments = await nocodb.list('PostAttachments', {
      where: "(mime_type,like,image/%)", limit: QUERY_LIMITS.max,
    });
    const thumbMap = {};
    for (const att of allAttachments) {
      if (att.post_id && !thumbMap[att.post_id]) thumbMap[att.post_id] = att.url;
    }

    // Collect unique IDs for name lookups
    const brandIds = new Set(), userIds = new Set(), trackIds = new Set();
    for (const p of filtered) {
      if (p.brand_id) brandIds.add(p.brand_id);
      if (p.assigned_to_id) userIds.add(p.assigned_to_id);
      if (p.created_by_user_id) userIds.add(p.created_by_user_id);
      if (p.track_id) trackIds.add(p.track_id);
    }
    const names = await batchResolveNames({
      brand: { table: 'Brands', ids: [...brandIds] },
      user: { table: 'Users', ids: [...userIds] },
      track: { table: 'CampaignTracks', ids: [...trackIds] },
    });
    const campaignName = await getRecordName('Campaigns', Number(req.params.id));

    res.json(filtered.map(p => ({
      ...p,
      assigned_to: p.assigned_to_id,
      campaign_name: campaignName,
      brand_name: names[`brand:${p.brand_id}`] || null,
      assigned_name: names[`user:${p.assigned_to_id}`] || null,
      creator_user_name: names[`user:${p.created_by_user_id}`] || null,
      track_name: names[`track:${p.track_id}`] || null,
      thumbnail_url: thumbMap[p.Id] || null,
    })));
  } catch (err) {
    res.status(500).json({ error: 'Failed to load campaign posts' });
  }
});

// ─── PROJECTS ───────────────────────────────────────────────────

app.get('/api/projects', requireAuth, async (req, res) => {
  try {
    const { status } = req.query;
    const whereParts = [];
    if (status) whereParts.push(`(status,eq,${sanitizeWhereValue(status)})`);
    if (req.query.brand_id) whereParts.push(`(brand_id,eq,${sanitizeWhereValue(req.query.brand_id)})`);
    if (req.query.owner_id) whereParts.push(`(owner_id,eq,${sanitizeWhereValue(req.query.owner_id)})`);
    const where = whereParts.length > 0 ? whereParts.join('~and') : undefined;

    let projects = await nocodb.list('Projects', { where, sort: '-CreatedAt', limit: QUERY_LIMITS.medium });

    // Team-based visibility filtering
    const userId = req.session.userId;
    if (req.session.userRole === 'manager') {
      const myTeamIds = await getUserTeamIds(userId);
      projects = projects.filter(p =>
        p.created_by_user_id === userId || p.owner_id === userId ||
        (p.team_id && myTeamIds.has(p.team_id)) || !p.team_id
      );
    } else if (req.session.userRole === 'contributor') {
      projects = projects.filter(p =>
        p.created_by_user_id === userId || p.owner_id === userId
      );
    }

    const brandIds = new Set(), userIds = new Set(), teamIds = new Set();
    for (const p of projects) {
      if (p.brand_id) brandIds.add(p.brand_id);
      if (p.owner_id) userIds.add(p.owner_id);
      if (p.created_by_user_id) userIds.add(p.created_by_user_id);
      if (p.team_id) teamIds.add(p.team_id);
    }
    const names = await batchResolveNames({
      brand: { table: 'Brands', ids: [...brandIds] },
      user: { table: 'Users', ids: [...userIds] },
      team: { table: 'Teams', ids: [...teamIds] },
    });

    res.json(projects.map(p => ({
      ...p,
      brand_name: names[`brand:${p.brand_id}`] || null,
      owner_name: names[`user:${p.owner_id}`] || null,
      creator_user_name: names[`user:${p.created_by_user_id}`] || null,
      team_name: names[`team:${p.team_id}`] || null,
      thumbnail_url: p.thumbnail ? `/api/uploads/${p.thumbnail}` : null,
    })));
  } catch (err) {
    res.status(500).json({ error: 'Failed to load projects' });
  }
});

app.get('/api/projects/:id', requireAuth, async (req, res) => {
  try {
    const project = await nocodb.get('Projects', req.params.id);
    if (!project) return res.status(404).json({ error: 'Project not found' });
    res.json({
      ...project,
      brand_name: await getRecordName('Brands', project.brand_id),
      owner_name: await getRecordName('Users', project.owner_id),
      creator_user_name: await getRecordName('Users', project.created_by_user_id),
      team_name: await getRecordName('Teams', project.team_id),
      thumbnail_url: project.thumbnail ? `/api/uploads/${project.thumbnail}` : null,
    });
  } catch (err) {
    res.status(500).json({ error: 'Failed to load project' });
  }
});

app.post('/api/projects', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  const { name, description, brand_id, owner_id, status, priority, start_date, due_date, team_id } = req.body;
  if (!name) return res.status(400).json({ error: 'Name is required' });

  try {
    const created = await nocodb.create('Projects', {
      name, description: description || null,
      status: status || 'active', priority: priority || 'medium',
      start_date: start_date || null, due_date: due_date || null,
      brand_id: brand_id ? Number(brand_id) : null,
      owner_id: owner_id ? Number(owner_id) : null,
      team_id: team_id ? Number(team_id) : null,
      created_by_user_id: req.session.userId,
    });

    const project = await nocodb.get('Projects', created.Id);
    res.status(201).json({
      ...project,
      brand_name: await getRecordName('Brands', project.brand_id),
      owner_name: await getRecordName('Users', project.owner_id),
      team_name: await getRecordName('Teams', project.team_id),
    });
  } catch (err) {
    console.error('Create project error:', err);
    res.status(500).json({ error: 'Failed to create project' });
  }
});

app.patch('/api/projects/:id', requireAuth, requireOwnerOrRole('projects', 'superadmin', 'manager'), async (req, res) => {
  const projectId = Number(req.params.id);
  try {
    const existing = await nocodb.get('Projects', projectId);
    if (!existing) return res.status(404).json({ error: 'Project not found' });

    const data = {};
    for (const f of ['name', 'description', 'status', 'priority', 'start_date', 'due_date', 'color']) {
      if (req.body[f] !== undefined) data[f] = req.body[f];
    }
    if (req.body.brand_id !== undefined) data.brand_id = req.body.brand_id ? Number(req.body.brand_id) : null;
    if (req.body.owner_id !== undefined) data.owner_id = req.body.owner_id ? Number(req.body.owner_id) : null;
    if (req.body.team_id !== undefined) data.team_id = req.body.team_id ? Number(req.body.team_id) : null;

    if (Object.keys(data).length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    await nocodb.update('Projects', projectId, data);

    const project = await nocodb.get('Projects', projectId);
    res.json({
      ...project,
      brand_name: await getRecordName('Brands', project.brand_id),
      owner_name: await getRecordName('Users', project.owner_id),
      team_name: await getRecordName('Teams', project.team_id),
    });
  } catch (err) {
    console.error('Update project error:', err);
    res.status(500).json({ error: 'Failed to update project' });
  }
});

app.delete('/api/projects/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Projects', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Project not found' });
    await nocodb.delete('Projects', req.params.id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete project' });
  }
});

app.post('/api/projects/:id/thumbnail', requireAuth, requireRole('superadmin', 'manager'), dynamicUpload('file'), async (req, res) => {
  try {
    const existing = await nocodb.get('Projects', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Project not found' });
    if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
    await nocodb.update('Projects', req.params.id, { thumbnail: req.file.filename });
    const project = await nocodb.get('Projects', req.params.id);
    res.json(project);
  } catch (err) {
    res.status(500).json({ error: 'Failed to upload thumbnail' });
  }
});

app.delete('/api/projects/:id/thumbnail', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Projects', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Project not found' });
    await nocodb.update('Projects', req.params.id, { thumbnail: null });
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to remove thumbnail' });
  }
});

// ─── TASKS ──────────────────────────────────────────────────────

app.get('/api/tasks', requireAuth, async (req, res) => {
  try {
    const { status, is_personal } = req.query;
    const whereParts = [];
    if (status) whereParts.push(`(status,eq,${sanitizeWhereValue(status)})`);
    if (is_personal !== undefined) {
      whereParts.push(`(is_personal,eq,${is_personal === 'true' || is_personal === '1' ? 'true' : 'false'})`);
    }
    if (req.query.project_id) whereParts.push(`(project_id,eq,${sanitizeWhereValue(req.query.project_id)})`);
    if (req.query.assigned_to) whereParts.push(`(assigned_to_id,eq,${sanitizeWhereValue(req.query.assigned_to)})`);
    if (req.query.priority) whereParts.push(`(priority,eq,${sanitizeWhereValue(req.query.priority)})`);
    if (req.query.created_by) whereParts.push(`(created_by_user_id,eq,${sanitizeWhereValue(req.query.created_by)})`);
    const where = whereParts.length > 0 ? whereParts.join('~and') : undefined;

    let tasks = await nocodb.list('Tasks', { where, sort: '-CreatedAt', limit: QUERY_LIMITS.max });

    // Team-based visibility filtering
    const userId = req.session.userId;
    if (req.session.userRole === 'manager') {
      const { teamProjectIds } = await getUserVisibilityContext(userId);
      tasks = tasks.filter(t =>
        t.created_by_user_id === userId || t.assigned_to_id === userId ||
        (t.project_id && teamProjectIds.has(t.project_id)) || !t.project_id
      );
    } else if (req.session.userRole === 'contributor') {
      tasks = tasks.filter(t =>
        t.created_by_user_id === userId || t.assigned_to_id === userId
      );
    }

    // Post-fetch date range filters
    if (req.query.due_date_from) {
      const from = new Date(req.query.due_date_from);
      tasks = tasks.filter(t => t.due_date && new Date(t.due_date) >= from);
    }
    if (req.query.due_date_to) {
      const to = new Date(req.query.due_date_to);
      to.setHours(23, 59, 59, 999);
      tasks = tasks.filter(t => t.due_date && new Date(t.due_date) <= to);
    }

    const projectIds = new Set(), userIds = new Set();
    for (const t of tasks) {
      if (t.project_id) projectIds.add(t.project_id);
      if (t.assigned_to_id) userIds.add(t.assigned_to_id);
      if (t.created_by_user_id) userIds.add(t.created_by_user_id);
    }
    const names = await batchResolveNames({
      project: { table: 'Projects', ids: [...projectIds] },
      user: { table: 'Users', ids: [...userIds] },
    });

    // Resolve brand info from projects
    const projectData = {};
    for (const pid of projectIds) {
      try {
        const proj = await nocodb.get('Projects', pid);
        if (proj) {
          projectData[pid] = {
            brand_id: proj.brand_id,
            brand_name: proj.brand_id ? await getRecordName('Brands', proj.brand_id) : null,
          };
        }
      } catch (err) { /* project may have been deleted — skip silently */ }
    }

    // Post-fetch brand filter (brand lives on the project)
    if (req.query.brand_id) {
      const brandId = Number(req.query.brand_id);
      tasks = tasks.filter(t => t.project_id && projectData[t.project_id]?.brand_id === brandId);
    }

    // Batch comment counts for all tasks
    const commentCounts = {};
    try {
      const allComments = await nocodb.list('Comments', {
        where: '(entity_type,eq,task)',
        fields: ['entity_id'],
        limit: QUERY_LIMITS.max,
      });
      for (const c of allComments) {
        commentCounts[c.entity_id] = (commentCounts[c.entity_id] || 0) + 1;
      }
    } catch (err) { console.error('Load task comment counts:', err.message); }

    res.json(tasks.map(t => ({
      ...t,
      assigned_to: t.assigned_to_id,
      project_name: names[`project:${t.project_id}`] || null,
      assigned_name: names[`user:${t.assigned_to_id}`] || null,
      creator_user_name: names[`user:${t.created_by_user_id}`] || null,
      brand_id: t.project_id && projectData[t.project_id] ? projectData[t.project_id].brand_id : null,
      brand_name: t.project_id && projectData[t.project_id] ? projectData[t.project_id].brand_name : null,
      comment_count: commentCounts[t.Id || t.id] || 0,
      thumbnail_url: t.thumbnail || null,
    })));
  } catch (err) {
    console.error('GET /tasks error:', err);
    res.status(500).json({ error: 'Failed to load tasks' });
  }
});

app.get('/api/tasks/my/:memberId', requireAuth, async (req, res) => {
  try {
    const tasks = await nocodb.list('Tasks', {
      where: `(is_personal,eq,true)~and(assigned_to_id,eq,${sanitizeWhereValue(req.params.memberId)})`,
      limit: QUERY_LIMITS.max,
    });

    const projectIds = new Set(), userIds = new Set();
    for (const t of tasks) {
      if (t.project_id) projectIds.add(t.project_id);
      if (t.created_by_user_id) userIds.add(t.created_by_user_id);
    }
    const names = await batchResolveNames({
      project: { table: 'Projects', ids: [...projectIds] },
      user: { table: 'Users', ids: [...userIds] },
    });

    res.json(tasks.map(t => ({
      ...t,
      assigned_to: t.assigned_to_id,
      project_name: names[`project:${t.project_id}`] || null,
      creator_user_name: names[`user:${t.created_by_user_id}`] || null,
    })));
  } catch (err) {
    res.status(500).json({ error: 'Failed to load tasks' });
  }
});

app.post('/api/tasks', requireAuth, async (req, res) => {
  const { title, description, project_id, assigned_to, status, priority, due_date, start_date, is_personal } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });

  try {
    const created = await nocodb.create('Tasks', {
      title, description: description || null,
      status: status || 'todo', priority: priority || 'medium',
      start_date: start_date || null, due_date: due_date || null, is_personal: !!is_personal,
      project_id: project_id ? Number(project_id) : null,
      assigned_to_id: assigned_to ? Number(assigned_to) : null,
      created_by_user_id: req.session.userId,
    });

    const task = await nocodb.get('Tasks', created.Id);
    res.status(201).json({
      ...task,
      assigned_to: task.assigned_to_id,
      project_name: await getRecordName('Projects', task.project_id),
      assigned_name: await getRecordName('Users', task.assigned_to_id),
      creator_user_name: await getRecordName('Users', task.created_by_user_id),
    });
    if (task.assigned_to_id) notify.notifyTaskAssigned({ task, assignerName: req.session.userName });
  } catch (err) {
    console.error('Create task error:', err);
    res.status(500).json({ error: 'Failed to create task' });
  }
});

// Bulk delete tasks
app.post('/api/tasks/bulk-delete', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const { ids } = req.body;
    if (!Array.isArray(ids) || ids.length === 0) return res.status(400).json({ error: 'ids array required' });
    for (const id of ids) {
      const atts = await nocodb.list('TaskAttachments', { where: `(task_id,eq,${sanitizeWhereValue(id)})`, limit: QUERY_LIMITS.large });
      if (atts.length > 0) await nocodb.bulkDelete('TaskAttachments', atts.map(a => ({ Id: a.Id })));
    }
    await nocodb.bulkDelete('Tasks', ids.map(id => ({ Id: id })));
    res.json({ deleted: ids.length });
  } catch (err) {
    console.error('Bulk delete tasks error:', err);
    res.status(500).json({ error: 'Failed to bulk delete tasks' });
  }
});

app.patch('/api/tasks/:id', requireAuth, requireOwnerOrRole('tasks', 'superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Tasks', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Task not found' });

    const data = {};
    for (const f of ['title', 'description', 'status', 'priority', 'start_date', 'due_date', 'color']) {
      if (req.body[f] !== undefined) data[f] = req.body[f];
    }
    if (req.body.is_personal !== undefined) data.is_personal = !!req.body.is_personal;
    if (req.body.project_id !== undefined) data.project_id = req.body.project_id ? Number(req.body.project_id) : null;
    if (req.body.assigned_to !== undefined) data.assigned_to_id = req.body.assigned_to ? Number(req.body.assigned_to) : null;

    // Handle completed_at
    if (req.body.status === 'done' && existing.status !== 'done') {
      data.completed_at = new Date().toISOString();
    } else if (req.body.status && req.body.status !== 'done' && existing.status === 'done') {
      data.completed_at = null;
    }

    if (Object.keys(data).length === 0) {
      return res.status(400).json({ error: 'No fields to update' });
    }

    await nocodb.update('Tasks', req.params.id, data);

    const task = await nocodb.get('Tasks', req.params.id);
    res.json({
      ...task,
      assigned_to: task.assigned_to_id,
      project_name: await getRecordName('Projects', task.project_id),
      assigned_name: await getRecordName('Users', task.assigned_to_id),
      creator_user_name: await getRecordName('Users', task.created_by_user_id),
    });
    // Notify on assignment change
    if (data.assigned_to_id && data.assigned_to_id !== existing.assigned_to_id) {
      notify.notifyTaskAssigned({ task, assignerName: req.session.userName });
    }
    // Notify on completion
    if (req.body.status === 'done' && existing.status !== 'done') {
      notify.notifyTaskCompleted({ task });
    }
  } catch (err) {
    console.error('Update task error:', err);
    res.status(500).json({ error: 'Failed to update task' });
  }
});

app.delete('/api/tasks/:id', requireAuth, requireOwnerOrRole('tasks', 'superadmin', 'manager'), async (req, res) => {
  try {
    await nocodb.delete('Tasks', req.params.id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete task' });
  }
});

// ─── TASK ATTACHMENTS ───────────────────────────────────────────

app.get('/api/tasks/:id/attachments', requireAuth, async (req, res) => {
  try {
    const attachments = await nocodb.list('TaskAttachments', {
      where: `(task_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: '-CreatedAt', limit: QUERY_LIMITS.large,
    });
    res.json(attachments);
  } catch (err) {
    res.status(500).json({ error: 'Failed to load attachments' });
  }
});

app.post('/api/tasks/:id/attachments', requireAuth, dynamicUpload('file'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });

  try {
    const task = await nocodb.get('Tasks', req.params.id);
    if (!task) return res.status(404).json({ error: 'Task not found' });

    const url = `/api/uploads/${req.file.filename}`;
    const created = await nocodb.create('TaskAttachments', {
      filename: req.file.filename,
      original_name: req.file.originalname,
      mime_type: req.file.mimetype,
      size: req.file.size,
      url,
      task_id: Number(req.params.id),
    });

    const attachment = await nocodb.get('TaskAttachments', created.Id);
    res.status(201).json(attachment);
  } catch (err) {
    console.error('Upload task attachment error:', err);
    res.status(500).json({ error: 'Failed to upload attachment' });
  }
});

app.delete('/api/task-attachments/:id', requireAuth, async (req, res) => {
  try {
    const attachment = await nocodb.get('TaskAttachments', req.params.id);
    if (!attachment) return res.status(404).json({ error: 'Attachment not found' });

    // Check if file is referenced elsewhere before deleting from disk
    const otherTaskAtts = await nocodb.list('TaskAttachments', {
      where: `(filename,eq,${attachment.filename})`, limit: 10,
    });
    const otherPostAtts = await nocodb.list('PostAttachments', {
      where: `(filename,eq,${attachment.filename})`, limit: 10,
    });
    const assets = await nocodb.list('Assets', {
      where: `(filename,eq,${attachment.filename})`, limit: 10,
    });
    if (otherTaskAtts.length <= 1 && otherPostAtts.length === 0 && assets.length === 0) {
      const filePath = path.join(uploadsDir, attachment.filename);
      if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
    }

    await nocodb.delete('TaskAttachments', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete task attachment error:', err);
    res.status(500).json({ error: 'Failed to delete attachment' });
  }
});

// Set a task's thumbnail from one of its image attachments
app.patch('/api/tasks/:id/thumbnail', requireAuth, async (req, res) => {
  try {
    const { attachment_id } = req.body;
    const task = await nocodb.get('Tasks', req.params.id);
    if (!task) return res.status(404).json({ error: 'Task not found' });

    if (attachment_id) {
      const att = await nocodb.get('TaskAttachments', attachment_id);
      if (!att) return res.status(404).json({ error: 'Attachment not found' });
      await nocodb.update('Tasks', req.params.id, { thumbnail: att.url || `/api/uploads/${att.filename}` });
    } else {
      await nocodb.update('Tasks', req.params.id, { thumbnail: null });
    }

    const updated = await nocodb.get('Tasks', req.params.id);
    res.json(updated);
  } catch (err) {
    res.status(500).json({ error: 'Failed to set thumbnail' });
  }
});

// ─── DASHBOARD ──────────────────────────────────────────────────

app.get('/api/dashboard', requireAuth, async (req, res) => {
  try {
    const isSuperadmin = req.session.userRole === 'superadmin';
    const userId = req.session.userId;

    // Fetch all data in parallel
    const [allPosts, allCampaigns, allTasks, allProjects, allUsers, allAssignments] = await Promise.all([
      nocodb.list('Posts', { limit: QUERY_LIMITS.max }),
      nocodb.list('Campaigns', { limit: QUERY_LIMITS.max }),
      nocodb.list('Tasks', { limit: QUERY_LIMITS.max }),
      nocodb.list('Projects', { limit: QUERY_LIMITS.max }),
      nocodb.list('Users', { limit: QUERY_LIMITS.large }),
      nocodb.list('CampaignAssignments', { limit: QUERY_LIMITS.max }),
    ]);

    // Build team-based scoping context
    let myTeamIds = new Set();
    let myCampaignIds = new Set();
    if (!isSuperadmin) {
      myTeamIds = await getUserTeamIds(userId);
      for (const c of allCampaigns) {
        if (c.created_by_user_id === userId) myCampaignIds.add(c.Id);
        if (req.session.userRole === 'manager' && c.team_id && myTeamIds.has(c.team_id)) myCampaignIds.add(c.Id);
      }
      for (const a of allAssignments) {
        if (a.member_id === userId && a.campaign_id) {
          myCampaignIds.add(a.campaign_id);
        }
      }
    }

    // Build team project IDs for managers
    let myProjectIds = new Set();
    if (!isSuperadmin) {
      for (const p of allProjects) {
        if (p.created_by_user_id === userId || p.owner_id === userId) myProjectIds.add(p.Id);
        if (req.session.userRole === 'manager' && p.team_id && myTeamIds.has(p.team_id)) myProjectIds.add(p.Id);
      }
    }

    // Posts
    let posts = allPosts;
    if (!isSuperadmin) {
      if (req.session.userRole === 'manager') {
        posts = allPosts.filter(p =>
          p.created_by_user_id === userId || p.assigned_to_id === userId ||
          (p.campaign_id && myCampaignIds.has(p.campaign_id)) || !p.campaign_id
        );
      } else {
        posts = allPosts.filter(p => p.created_by_user_id === userId || p.assigned_to_id === userId);
      }
    }
    const postsByStatus = {};
    for (const p of posts) {
      postsByStatus[p.status] = (postsByStatus[p.status] || 0) + 1;
    }

    // Campaigns
    let campaigns = allCampaigns;
    if (!isSuperadmin) {
      campaigns = allCampaigns.filter(c => myCampaignIds.has(c.Id) || c.created_by_user_id === userId);
    }
    const activeCampaigns = campaigns.filter(c => c.status === 'active').length;

    // Tasks
    let tasks = allTasks;
    if (!isSuperadmin) {
      if (req.session.userRole === 'manager') {
        tasks = allTasks.filter(t =>
          t.created_by_user_id === userId || t.assigned_to_id === userId ||
          (t.project_id && myProjectIds.has(t.project_id)) || !t.project_id
        );
      } else {
        tasks = allTasks.filter(t =>
          t.created_by_user_id === userId || t.assigned_to_id === userId
        );
      }
    }
    const overdueTasks = tasks.filter(t => t.due_date && new Date(t.due_date) < new Date() && t.status !== 'done').length;
    const tasksByStatus = {};
    for (const t of tasks) {
      tasksByStatus[t.status] = (tasksByStatus[t.status] || 0) + 1;
    }

    // Projects
    let projects = allProjects;
    if (!isSuperadmin) {
      projects = allProjects.filter(p => myProjectIds.has(p.Id) || p.created_by_user_id === userId || p.owner_id === userId);
    }
    const activeProjects = projects.filter(p => p.status === 'active').length;

    // Team workload (superadmin only)
    const teamWorkload = isSuperadmin ? allUsers.filter(u => u.team_role).map(u => {
      const userTasks = allTasks.filter(t => t.assigned_to_id === u.Id);
      const userPosts = allPosts.filter(p => p.assigned_to_id === u.Id);
      return {
        id: u.Id, name: u.name, role: u.team_role,
        active_tasks: userTasks.filter(t => t.status !== 'done').length,
        completed_tasks: userTasks.filter(t => t.status === 'done').length,
        active_posts: userPosts.filter(p => !['published', 'rejected'].includes(p.status)).length,
      };
    }).sort((a, b) => b.active_tasks - a.active_tasks) : [];

    // Recent posts (last 5)
    const recentPostsRaw = posts.slice(0, 5);
    const recentPosts = [];
    for (const p of recentPostsRaw) {
      recentPosts.push({
        ...p,
        assigned_to: p.assigned_to_id,
        brand_name: await getRecordName('Brands', p.brand_id),
        assigned_name: await getRecordName('Users', p.assigned_to_id),
      });
    }

    // Upcoming campaigns
    const now = new Date().toISOString().split('T')[0];
    const upcomingRaw = campaigns
      .filter(c => c.end_date >= now)
      .sort((a, b) => (a.start_date || '').localeCompare(b.start_date || ''))
      .slice(0, 5);
    const upcomingCampaigns = [];
    for (const c of upcomingRaw) {
      upcomingCampaigns.push({ ...c, brand_name: await getRecordName('Brands', c.brand_id) });
    }

    res.json({
      posts: { total: posts.length, byStatus: postsByStatus },
      campaigns: { total: campaigns.length, active: activeCampaigns },
      tasks: { overdue: overdueTasks, byStatus: tasksByStatus },
      projects: { active: activeProjects },
      teamWorkload,
      recentPosts,
      upcomingCampaigns,
    });
  } catch (err) {
    console.error('Dashboard error:', err);
    res.status(500).json({ error: 'Failed to load dashboard' });
  }
});

// ─── COMMENTS / DISCUSSIONS ─────────────────────────────────────

app.get('/api/comments/:entityType/:entityId', requireAuth, async (req, res) => {
  const { entityType, entityId } = req.params;
  if (!COMMENT_ENTITY_TYPES.has(entityType)) return res.status(400).json({ error: 'Invalid entity type' });

  try {
    const comments = await nocodb.list('Comments', {
      where: `(entity_type,eq,${sanitizeWhereValue(entityType)})~and(entity_id,eq,${sanitizeWhereValue(entityId)})`,
      sort: 'CreatedAt', limit: QUERY_LIMITS.large,
    });

    // Enrich with user data
    const enriched = [];
    for (const c of comments) {
      let userName = null, userAvatar = null;
      if (c.user_id) {
        try {
          const u = await nocodb.get('Users', c.user_id);
          userName = u.name;
          userAvatar = u.avatar;
        } catch (err) { console.error('Resolve comment user:', err.message); }
      }
      enriched.push({ ...c, user_name: userName, user_avatar: userAvatar });
    }
    res.json(enriched);
  } catch (err) {
    res.status(500).json({ error: 'Failed to load comments' });
  }
});

app.post('/api/comments/:entityType/:entityId', requireAuth, async (req, res) => {
  const { entityType, entityId } = req.params;
  const { content } = req.body;
  if (!COMMENT_ENTITY_TYPES.has(entityType)) return res.status(400).json({ error: 'Invalid entity type' });
  if (!content || !content.trim()) return res.status(400).json({ error: 'Content is required' });

  try {
    const created = await nocodb.create('Comments', {
      entity_type: entityType,
      entity_id: Number(entityId),
      content: content.trim(),
      user_id: req.session.userId,
    });

    const comment = await nocodb.get('Comments', created.Id);
    const user = await nocodb.get('Users', req.session.userId);
    res.status(201).json({
      ...comment, user_id: req.session.userId,
      user_name: user.name, user_avatar: user.avatar,
    });
  } catch (err) {
    res.status(500).json({ error: 'Failed to create comment' });
  }
});

app.patch('/api/comments/:id', requireAuth, async (req, res) => {
  try {
    const comment = await nocodb.get('Comments', req.params.id);
    if (!comment) return res.status(404).json({ error: 'Comment not found' });

    if (comment.user_id !== req.session.userId) return res.status(403).json({ error: 'You can only edit your own comments' });

    const { content } = req.body;
    if (!content || !content.trim()) return res.status(400).json({ error: 'Content is required' });

    await nocodb.update('Comments', req.params.id, { content: content.trim() });
    const updated = await nocodb.get('Comments', req.params.id);
    const user = await nocodb.get('Users', req.session.userId);
    res.json({ ...updated, user_id: req.session.userId, user_name: user.name, user_avatar: user.avatar });
  } catch (err) {
    res.status(500).json({ error: 'Failed to update comment' });
  }
});

app.delete('/api/comments/:id', requireAuth, async (req, res) => {
  try {
    const comment = await nocodb.get('Comments', req.params.id);
    if (!comment) return res.status(404).json({ error: 'Comment not found' });

    if (comment.user_id !== req.session.userId && req.session.userRole !== 'superadmin' && req.session.userRole !== 'manager') {
      return res.status(403).json({ error: 'You can only delete your own comments' });
    }
    await nocodb.delete('Comments', req.params.id);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete comment' });
  }
});

// ─── SHARED HELPERS ─────────────────────────────────────────────

// Get set of campaign IDs a user has access to
async function getUserCampaignIds(userId) {
  const [campaigns, assignments] = await Promise.all([
    nocodb.list('Campaigns', { limit: QUERY_LIMITS.max }),
    nocodb.list('CampaignAssignments', { limit: QUERY_LIMITS.max }),
  ]);
  const ids = new Set();
  for (const c of campaigns) {
    if (c.created_by_user_id === userId) ids.add(c.Id);
  }
  for (const a of assignments) {
    if (a.member_id === userId && a.campaign_id) {
      ids.add(a.campaign_id);
    }
  }
  return ids;
}

// ─── ERROR HANDLING ─────────────────────────────────────────────

// ─── TEAMS ──────────────────────────────────────────────────────

app.get('/api/teams', requireAuth, async (req, res) => {
  try {
    const teams = await nocodb.list('Teams', { sort: 'name', limit: QUERY_LIMITS.medium });
    const members = await nocodb.list('TeamMembers', { limit: QUERY_LIMITS.max });
    const result = teams.map(t => {
      const teamMembers = members.filter(m => m.team_id === t.Id);
      return {
        ...t, id: t.Id, _id: t.Id,
        member_ids: teamMembers.map(m => m.user_id),
        member_count: teamMembers.length,
      };
    });
    res.json(result);
  } catch (err) {
    console.error('Teams list error:', err);
    res.status(500).json({ error: 'Failed to load teams' });
  }
});

app.post('/api/teams', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  const { name, description, member_ids } = req.body;
  if (!name) return res.status(400).json({ error: 'Name is required' });
  try {
    const created = await nocodb.create('Teams', { name, description: description || null });
    if (member_ids && member_ids.length > 0) {
      await nocodb.bulkCreate('TeamMembers', member_ids.map(uid => ({ team_id: created.Id, user_id: uid })));
    }
    const team = await nocodb.get('Teams', created.Id);
    res.status(201).json({ ...team, id: team.Id, _id: team.Id, member_ids: member_ids || [] });
  } catch (err) {
    console.error('Create team error:', err);
    res.status(500).json({ error: 'Failed to create team' });
  }
});

app.patch('/api/teams/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Teams', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Team not found' });
    const data = {};
    if (req.body.name !== undefined) data.name = req.body.name;
    if (req.body.description !== undefined) data.description = req.body.description;
    if (Object.keys(data).length > 0) await nocodb.update('Teams', req.params.id, data);

    // Sync members if provided
    if (req.body.member_ids !== undefined) {
      const oldMembers = await nocodb.list('TeamMembers', { where: `(team_id,eq,${sanitizeWhereValue(req.params.id)})`, limit: QUERY_LIMITS.max });
      if (oldMembers.length > 0) {
        await nocodb.bulkDelete('TeamMembers', oldMembers.map(m => ({ Id: m.Id })));
      }
      if (req.body.member_ids.length > 0) {
        await nocodb.bulkCreate('TeamMembers', req.body.member_ids.map(uid => ({ team_id: Number(req.params.id), user_id: uid })));
      }
    }

    const team = await nocodb.get('Teams', req.params.id);
    const members = await nocodb.list('TeamMembers', { where: `(team_id,eq,${sanitizeWhereValue(req.params.id)})`, limit: QUERY_LIMITS.max });
    res.json({ ...team, id: team.Id, _id: team.Id, member_ids: members.map(m => m.user_id) });
  } catch (err) {
    console.error('Update team error:', err);
    res.status(500).json({ error: 'Failed to update team' });
  }
});

app.delete('/api/teams/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const existing = await nocodb.get('Teams', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Team not found' });
    const members = await nocodb.list('TeamMembers', { where: `(team_id,eq,${sanitizeWhereValue(req.params.id)})`, limit: QUERY_LIMITS.max });
    if (members.length > 0) {
      await nocodb.bulkDelete('TeamMembers', members.map(m => ({ Id: m.Id })));
    }
    await nocodb.delete('Teams', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete team error:', err);
    res.status(500).json({ error: 'Failed to delete team' });
  }
});

app.post('/api/teams/:id/members', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  const { user_id } = req.body;
  if (!user_id) return res.status(400).json({ error: 'user_id is required' });
  try {
    await nocodb.create('TeamMembers', { team_id: Number(req.params.id), user_id: Number(user_id) });
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to add member' });
  }
});

app.delete('/api/teams/:id/members/:userId', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const entries = await nocodb.list('TeamMembers', {
      where: `(team_id,eq,${sanitizeWhereValue(req.params.id)})~and(user_id,eq,${sanitizeWhereValue(req.params.userId)})`,
      limit: 10,
    });
    if (entries.length > 0) {
      await nocodb.bulkDelete('TeamMembers', entries.map(e => ({ Id: e.Id })));
    }
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to remove member' });
  }
});

// ─── ARTEFACTS (Content Approval System) ───────────────────────

app.get('/api/artefacts', requireAuth, async (req, res) => {
  try {
    const { brand, status, post } = req.query;
    const whereParts = [];
    if (brand) whereParts.push(`(brand_id,eq,${sanitizeWhereValue(brand)})`);
    if (status) whereParts.push(`(status,eq,${sanitizeWhereValue(status)})`);
    if (post) whereParts.push(`(post_id,eq,${sanitizeWhereValue(post)})`);
    const where = whereParts.length > 0 ? whereParts.join('~and') : undefined;

    let artefacts = await nocodb.list('Artefacts', { where, sort: '-UpdatedAt', limit: QUERY_LIMITS.medium });

    // Team-based visibility filtering
    const userId = req.session.userId;
    if (req.session.userRole === 'manager') {
      const { teamProjectIds, teamCampaignIds } = await getUserVisibilityContext(userId);
      artefacts = artefacts.filter(a =>
        a.created_by_user_id === userId ||
        (a.project_id && teamProjectIds.has(a.project_id)) ||
        (a.campaign_id && teamCampaignIds.has(a.campaign_id)) ||
        (!a.project_id && !a.campaign_id)
      );
    } else if (req.session.userRole === 'contributor') {
      artefacts = artefacts.filter(a => a.created_by_user_id === userId);
    }

    // Enrich with names
    const brandIds = new Set(), userIds = new Set(), postIds = new Set(), projectIds = new Set(), campaignIds = new Set();
    for (const a of artefacts) {
      if (a.brand_id) brandIds.add(a.brand_id);
      if (a.created_by_user_id) userIds.add(a.created_by_user_id);
      if (a.post_id) postIds.add(a.post_id);
      if (a.project_id) projectIds.add(a.project_id);
      if (a.campaign_id) campaignIds.add(a.campaign_id);
      // Collect approver user IDs
      if (a.approver_ids) {
        for (const id of a.approver_ids.split(',').map(s => s.trim()).filter(Boolean)) {
          userIds.add(Number(id));
        }
      }
    }
    const names = await batchResolveNames({
      brand: { table: 'Brands', ids: [...brandIds] },
      user: { table: 'Users', ids: [...userIds] },
      post: { table: 'Posts', ids: [...postIds] },
      project: { table: 'Projects', ids: [...projectIds] },
      campaign: { table: 'Campaigns', ids: [...campaignIds] },
    });

    res.json(artefacts.map(a => {
      const approverIdList = a.approver_ids ? a.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
      return {
        ...a,
        brand_name: names[`brand:${a.brand_id}`] || null,
        creator_name: names[`user:${a.created_by_user_id}`] || null,
        post_title: names[`post:${a.post_id}`] || null,
        project_name: names[`project:${a.project_id}`] || null,
        campaign_name: names[`campaign:${a.campaign_id}`] || null,
        approvers: approverIdList.map(id => ({ id: Number(id), name: names[`user:${Number(id)}`] || null })),
      };
    }));
  } catch (err) {
    console.error('GET /artefacts error:', err);
    res.status(500).json({ error: 'Failed to load artefacts' });
  }
});

app.post('/api/artefacts', requireAuth, async (req, res) => {
  const { title, description, type, brand_id, content, project_id, campaign_id, approver_ids } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });

  try {
    const createData = {
      title,
      description: description || null,
      type: type || 'other',
      status: 'draft',
      content: content || null,
      brand_id: brand_id ? Number(brand_id) : null,
      project_id: project_id ? Number(project_id) : null,
      campaign_id: campaign_id ? Number(campaign_id) : null,
      approver_ids: approver_ids || null,
      created_by_user_id: req.session.userId,
      current_version: 1,
    };
    console.log('[POST /artefacts] Creating with:', JSON.stringify({ approver_ids: createData.approver_ids, project_id: createData.project_id, campaign_id: createData.campaign_id }));
    const created = await nocodb.create('Artefacts', createData);
    console.log('[POST /artefacts] NocoDB returned:', JSON.stringify({ Id: created.Id, approver_ids: created.approver_ids }));

    // Auto-create version 1
    await nocodb.create('ArtefactVersions', {
      artefact_id: created.Id,
      version_number: 1,
      created_by_user_id: req.session.userId,
      created_at: new Date().toISOString(),
      notes: 'Initial version',
    });

    const artefact = await nocodb.get('Artefacts', created.Id);
    console.log('[POST /artefacts] After re-read:', JSON.stringify({ Id: artefact.Id, approver_ids: artefact.approver_ids }));
    const approverIdList = artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
    const approvers = [];
    for (const id of approverIdList) {
      approvers.push({ id: Number(id), name: await getRecordName('Users', Number(id)) });
    }
    res.status(201).json({
      ...artefact,
      brand_name: await getRecordName('Brands', artefact.brand_id),
      creator_name: await getRecordName('Users', artefact.created_by_user_id),
      project_name: await getRecordName('Projects', artefact.project_id),
      campaign_name: await getRecordName('Campaigns', artefact.campaign_id),
      approvers,
    });
  } catch (err) {
    console.error('Create artefact error:', err);
    res.status(500).json({ error: 'Failed to create artefact' });
  }
});

// Bulk delete artefacts
app.post('/api/artefacts/bulk-delete', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const { ids } = req.body;
    if (!Array.isArray(ids) || ids.length === 0) return res.status(400).json({ error: 'ids array required' });
    for (const id of ids) {
      // Delete attachments + files
      const atts = await nocodb.list('ArtefactAttachments', { where: `(artefact_id,eq,${sanitizeWhereValue(id)})`, limit: QUERY_LIMITS.large });
      for (const att of atts) {
        if (att.filename) {
          const filePath = path.join(uploadsDir, att.filename);
          if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
        }
      }
      if (atts.length > 0) await nocodb.bulkDelete('ArtefactAttachments', atts.map(a => ({ Id: a.Id })));
      // Delete versions + version texts
      const versions = await nocodb.list('ArtefactVersions', { where: `(artefact_id,eq,${sanitizeWhereValue(id)})`, limit: QUERY_LIMITS.large });
      for (const ver of versions) {
        const texts = await nocodb.list('ArtefactVersionTexts', { where: `(version_id,eq,${sanitizeWhereValue(ver.Id)})`, limit: QUERY_LIMITS.large });
        if (texts.length > 0) await nocodb.bulkDelete('ArtefactVersionTexts', texts.map(t => ({ Id: t.Id })));
      }
      if (versions.length > 0) await nocodb.bulkDelete('ArtefactVersions', versions.map(v => ({ Id: v.Id })));
    }
    await nocodb.bulkDelete('Artefacts', ids.map(id => ({ Id: id })));
    res.json({ deleted: ids.length });
  } catch (err) {
    console.error('Bulk delete artefacts error:', err);
    res.status(500).json({ error: 'Failed to bulk delete artefacts' });
  }
});

app.patch('/api/artefacts/:id', requireAuth, async (req, res) => {
  try {
    const existing = await nocodb.get('Artefacts', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Artefact not found' });

    // Permission check: owner or manager+
    if (req.session.userRole === 'contributor' && existing.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only modify your own artefacts' });
    }

    const data = {};
    for (const f of ['title', 'description', 'type', 'status', 'content', 'feedback']) {
      if (req.body[f] !== undefined) data[f] = req.body[f];
    }
    if (req.body.brand_id !== undefined) data.brand_id = req.body.brand_id ? Number(req.body.brand_id) : null;
    if (req.body.post_id !== undefined) data.post_id = req.body.post_id ? Number(req.body.post_id) : null;
    if (req.body.project_id !== undefined) data.project_id = req.body.project_id ? Number(req.body.project_id) : null;
    if (req.body.campaign_id !== undefined) data.campaign_id = req.body.campaign_id ? Number(req.body.campaign_id) : null;
    if (req.body.approver_ids !== undefined) data.approver_ids = req.body.approver_ids || null;

    if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });

    console.log(`[PATCH /artefacts/${req.params.id}] Updating:`, JSON.stringify(data));
    await nocodb.update('Artefacts', req.params.id, data);

    const artefact = await nocodb.get('Artefacts', req.params.id);
    console.log(`[PATCH /artefacts/${req.params.id}] After re-read: approver_ids=${artefact.approver_ids}`);
    const approverIdList = artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
    const approvers = [];
    for (const id of approverIdList) {
      approvers.push({ id: Number(id), name: await getRecordName('Users', Number(id)) });
    }
    res.json({
      ...artefact,
      brand_name: await getRecordName('Brands', artefact.brand_id),
      creator_name: await getRecordName('Users', artefact.created_by_user_id),
      post_title: await getRecordName('Posts', artefact.post_id),
      project_name: await getRecordName('Projects', artefact.project_id),
      campaign_name: await getRecordName('Campaigns', artefact.campaign_id),
      approvers,
    });
  } catch (err) {
    console.error('Update artefact error:', err);
    res.status(500).json({ error: 'Failed to update artefact' });
  }
});

app.delete('/api/artefacts/:id', requireAuth, async (req, res) => {
  try {
    const existing = await nocodb.get('Artefacts', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Artefact not found' });

    // Permission check: owner or manager+
    if (req.session.userRole === 'contributor' && existing.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only delete your own artefacts' });
    }
    if (!['superadmin', 'manager'].includes(req.session.userRole) && existing.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }

    // Delete attachments
    const attachments = await nocodb.list('ArtefactAttachments', {
      where: `(artefact_id,eq,${sanitizeWhereValue(req.params.id)})`,
      limit: QUERY_LIMITS.large,
    });
    for (const att of attachments) {
      const filePath = path.join(uploadsDir, att.filename);
      if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
      await nocodb.delete('ArtefactAttachments', att.Id);
    }

    await nocodb.delete('Artefacts', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete artefact error:', err);
    res.status(500).json({ error: 'Failed to delete artefact' });
  }
});

// Submit for review - generates approval token
app.post('/api/artefacts/:id/submit-review', requireAuth, async (req, res) => {
  try {
    const existing = await nocodb.get('Artefacts', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Artefact not found' });

    if (req.session.userRole === 'contributor' && existing.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only submit your own artefacts' });
    }

    const token = require('crypto').randomUUID();
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + DEFAULTS.tokenExpiryDays);

    await nocodb.update('Artefacts', req.params.id, {
      status: 'pending_review',
      approval_token: token,
      token_expires_at: expiresAt.toISOString(),
      review_version: existing.current_version || 1,
    });

    const reviewUrl = `${req.protocol}://${req.get('host')}/review/${token}`;
    res.json({ success: true, token, reviewUrl, expiresAt: expiresAt.toISOString() });
    notify.notifyReviewSubmitted({ type: 'artefact', record: existing, reviewUrl });
  } catch (err) {
    console.error('Submit review error:', err);
    res.status(500).json({ error: 'Failed to submit for review' });
  }
});

// Link artefact to post
app.post('/api/artefacts/:id/link-post', requireAuth, async (req, res) => {
  const { post_id } = req.body;
  if (!post_id) return res.status(400).json({ error: 'post_id is required' });

  try {
    const artefact = await nocodb.get('Artefacts', req.params.id);
    if (!artefact) return res.status(404).json({ error: 'Artefact not found' });

    if (artefact.status !== 'approved') {
      return res.status(400).json({ error: 'Only approved artefacts can be linked to posts' });
    }

    await nocodb.update('Artefacts', req.params.id, { post_id: Number(post_id) });
    const updated = await nocodb.get('Artefacts', req.params.id);
    res.json({
      ...updated,
      brand_name: await getRecordName('Brands', updated.brand_id),
      creator_name: await getRecordName('Users', updated.created_by_user_id),
      post_title: await getRecordName('Posts', updated.post_id),
    });
  } catch (err) {
    console.error('Link post error:', err);
    res.status(500).json({ error: 'Failed to link artefact to post' });
  }
});

// ─── ARTEFACT VERSIONS ──────────────────────────────────────────

// List all versions for an artefact
app.get('/api/artefacts/:id/versions', requireAuth, async (req, res) => {
  try {
    const versions = await nocodb.list('ArtefactVersions', {
      where: `(artefact_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: 'version_number',
      limit: QUERY_LIMITS.large,
    });

    // Enrich with creator names
    const enriched = [];
    for (const v of versions) {
      const creatorName = await getRecordName('Users', v.created_by_user_id);
      enriched.push({ ...v, creator_name: creatorName });
    }

    res.json(enriched);
  } catch (err) {
    console.error('List versions error:', err);
    res.status(500).json({ error: 'Failed to load versions' });
  }
});

// Create new version
app.post('/api/artefacts/:id/versions', requireAuth, async (req, res) => {
  const { notes, copy_from_previous } = req.body;

  try {
    const artefact = await nocodb.get('Artefacts', req.params.id);
    if (!artefact) return res.status(404).json({ error: 'Artefact not found' });

    if (req.session.userRole === 'contributor' && artefact.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only create versions for your own artefacts' });
    }

    // Get current max version number
    const versions = await nocodb.list('ArtefactVersions', {
      where: `(artefact_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: '-version_number',
      limit: 1,
    });
    const newVersionNumber = versions.length > 0 ? versions[0].version_number + 1 : 1;

    const created = await nocodb.create('ArtefactVersions', {
      artefact_id: Number(req.params.id),
      version_number: newVersionNumber,
      created_by_user_id: req.session.userId,
      created_at: new Date().toISOString(),
      notes: notes || `Version ${newVersionNumber}`,
    });

    // Update artefact current_version
    await nocodb.update('Artefacts', req.params.id, { current_version: newVersionNumber });

    // Copy texts from previous version if requested (for copy type)
    if (copy_from_previous && artefact.type === 'copy' && versions.length > 0) {
      const prevVersionId = versions[0].Id;
      const prevTexts = await nocodb.list('ArtefactVersionTexts', {
        where: `(version_id,eq,${prevVersionId})`,
        limit: QUERY_LIMITS.large,
      });

      for (const text of prevTexts) {
        await nocodb.create('ArtefactVersionTexts', {
          version_id: created.Id,
          language_code: text.language_code,
          language_label: text.language_label,
          content: text.content,
        });
      }
    }

    const version = await nocodb.get('ArtefactVersions', created.Id);
    const creatorName = await getRecordName('Users', version.created_by_user_id);
    res.status(201).json({ ...version, creator_name: creatorName });
  } catch (err) {
    console.error('Create version error:', err);
    res.status(500).json({ error: 'Failed to create version' });
  }
});

// Get specific version with texts/attachments
app.get('/api/artefacts/:id/versions/:versionId', requireAuth, async (req, res) => {
  try {
    const version = await nocodb.get('ArtefactVersions', req.params.versionId);
    if (!version) return res.status(404).json({ error: 'Version not found' });
    if (version.artefact_id !== Number(req.params.id)) {
      return res.status(400).json({ error: 'Version does not belong to this artefact' });
    }

    // Get texts and attachments
    const [texts, attachments] = await Promise.all([
      nocodb.list('ArtefactVersionTexts', {
        where: `(version_id,eq,${sanitizeWhereValue(req.params.versionId)})`,
        limit: QUERY_LIMITS.large,
      }),
      nocodb.list('ArtefactAttachments', {
        where: `(version_id,eq,${sanitizeWhereValue(req.params.versionId)})`,
        limit: QUERY_LIMITS.large,
      }),
    ]);

    const creatorName = await getRecordName('Users', version.created_by_user_id);

    res.json({
      ...version,
      creator_name: creatorName,
      texts,
      attachments: attachments.map(a => ({
        ...a,
        url: a.drive_url || `/api/uploads/${a.filename}`,
      })),
    });
  } catch (err) {
    console.error('Get version error:', err);
    res.status(500).json({ error: 'Failed to load version' });
  }
});

// Add/update language entry for version
app.post('/api/artefacts/:id/versions/:versionId/texts', requireAuth, async (req, res) => {
  const { language_code, language_label, content } = req.body;
  if (!language_code || !language_label || !content) {
    return res.status(400).json({ error: 'language_code, language_label, and content are required' });
  }

  try {
    const artefact = await nocodb.get('Artefacts', req.params.id);
    if (!artefact) return res.status(404).json({ error: 'Artefact not found' });

    if (req.session.userRole === 'contributor' && artefact.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only manage texts for your own artefacts' });
    }

    // Check if language already exists for this version
    const existing = await nocodb.list('ArtefactVersionTexts', {
      where: `(version_id,eq,${sanitizeWhereValue(req.params.versionId)})~and(language_code,eq,${sanitizeWhereValue(language_code)})`,
      limit: 1,
    });

    let text;
    if (existing.length > 0) {
      // Update existing
      await nocodb.update('ArtefactVersionTexts', existing[0].Id, {
        language_label,
        content,
      });
      text = await nocodb.get('ArtefactVersionTexts', existing[0].Id);
    } else {
      // Create new
      const created = await nocodb.create('ArtefactVersionTexts', {
        version_id: Number(req.params.versionId),
        language_code,
        language_label,
        content,
      });
      text = await nocodb.get('ArtefactVersionTexts', created.Id);
    }

    res.json(text);
  } catch (err) {
    console.error('Add/update text error:', err);
    res.status(500).json({ error: 'Failed to add/update text' });
  }
});

// Delete language entry
app.delete('/api/artefact-version-texts/:id', requireAuth, async (req, res) => {
  try {
    const text = await nocodb.get('ArtefactVersionTexts', req.params.id);
    if (!text) return res.status(404).json({ error: 'Text not found' });

    // Permission check via version -> artefact
    const version = await nocodb.get('ArtefactVersions', text.version_id);
    const artefact = await nocodb.get('Artefacts', version.artefact_id);

    if (req.session.userRole === 'contributor' && artefact.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only manage texts for your own artefacts' });
    }

    await nocodb.delete('ArtefactVersionTexts', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete text error:', err);
    res.status(500).json({ error: 'Failed to delete text' });
  }
});

// Upload attachment to specific version
app.post('/api/artefacts/:id/versions/:versionId/attachments', requireAuth, dynamicUpload('file'), async (req, res) => {
  try {
    const artefact = await nocodb.get('Artefacts', req.params.id);
    if (!artefact) return res.status(404).json({ error: 'Artefact not found' });

    if (req.session.userRole === 'contributor' && artefact.created_by_user_id !== req.session.userId) {
      if (req.file) fs.unlinkSync(path.join(uploadsDir, req.file.filename));
      return res.status(403).json({ error: 'You can only manage attachments on your own artefacts' });
    }

    const { drive_url } = req.body;

    // Either file upload or drive URL
    if (!req.file && !drive_url) {
      return res.status(400).json({ error: 'Either file upload or drive_url is required' });
    }

    const data = {
      artefact_id: Number(req.params.id),
      version_id: Number(req.params.versionId),
    };

    if (req.file) {
      data.filename = req.file.filename;
      data.original_name = req.file.originalname;
      data.mime_type = req.file.mimetype;
      data.size = req.file.size;
    }

    if (drive_url) {
      data.drive_url = drive_url;
      data.original_name = 'Google Drive Video';
      data.mime_type = 'video/*';
    }

    const created = await nocodb.create('ArtefactAttachments', data);
    const attachment = await nocodb.get('ArtefactAttachments', created.Id);

    res.status(201).json({
      ...attachment,
      url: attachment.drive_url || `/api/uploads/${attachment.filename}`,
    });
  } catch (err) {
    console.error('Upload attachment error:', err);
    res.status(500).json({ error: 'Failed to upload attachment' });
  }
});

// Get comments for specific version
app.get('/api/artefacts/:id/versions/:versionId/comments', requireAuth, async (req, res) => {
  try {
    const version = await nocodb.get('ArtefactVersions', req.params.versionId);
    if (!version) return res.status(404).json({ error: 'Version not found' });

    const comments = await nocodb.list('Comments', {
      where: `(entity_type,eq,artefact)~and(entity_id,eq,${sanitizeWhereValue(req.params.id)})~and(version_number,eq,${version.version_number})`,
      sort: 'CreatedAt',
      limit: QUERY_LIMITS.large,
    });

    // Enrich with user data
    const enriched = [];
    for (const c of comments) {
      let userName = null, userAvatar = null;
      if (c.user_id) {
        try {
          const u = await nocodb.get('Users', c.user_id);
          userName = u.name;
          userAvatar = u.avatar;
        } catch (err) { console.error('Resolve version comment user:', err.message); }
      }
      enriched.push({ ...c, user_name: userName, user_avatar: userAvatar });
    }

    res.json(enriched);
  } catch (err) {
    console.error('Get comments error:', err);
    res.status(500).json({ error: 'Failed to load comments' });
  }
});

// Add comment to specific version
app.post('/api/artefacts/:id/versions/:versionId/comments', requireAuth, async (req, res) => {
  const { content } = req.body;
  if (!content || !content.trim()) {
    return res.status(400).json({ error: 'Content is required' });
  }

  try {
    const version = await nocodb.get('ArtefactVersions', req.params.versionId);
    if (!version) return res.status(404).json({ error: 'Version not found' });

    const created = await nocodb.create('Comments', {
      entity_type: 'artefact',
      entity_id: Number(req.params.id),
      version_number: version.version_number,
      content: content.trim(),
      user_id: req.session.userId,
    });

    const comment = await nocodb.get('Comments', created.Id);
    const user = await nocodb.get('Users', req.session.userId);

    res.status(201).json({
      ...comment,
      user_id: req.session.userId,
      user_name: user.name,
      user_avatar: user.avatar,
    });
  } catch (err) {
    console.error('Add comment error:', err);
    res.status(500).json({ error: 'Failed to add comment' });
  }
});

// Artefact attachments (legacy/general — now version-aware)
app.get('/api/artefacts/:id/attachments', requireAuth, async (req, res) => {
  try {
    const attachments = await nocodb.list('ArtefactAttachments', {
      where: `(artefact_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: '-CreatedAt',
      limit: QUERY_LIMITS.large,
    });
    res.json(attachments.map(a => ({
      ...a,
      url: a.drive_url || `/api/uploads/${a.filename}`,
    })));
  } catch (err) {
    res.status(500).json({ error: 'Failed to load attachments' });
  }
});

app.post('/api/artefacts/:id/attachments', requireAuth, dynamicUpload('file'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });

  try {
    const artefact = await nocodb.get('Artefacts', req.params.id);
    if (!artefact) return res.status(404).json({ error: 'Artefact not found' });

    if (req.session.userRole === 'contributor' && artefact.created_by_user_id !== req.session.userId) {
      fs.unlinkSync(path.join(uploadsDir, req.file.filename));
      return res.status(403).json({ error: 'You can only manage attachments on your own artefacts' });
    }

    const created = await nocodb.create('ArtefactAttachments', {
      filename: req.file.filename,
      original_name: req.file.originalname,
      mime_type: req.file.mimetype,
      size: req.file.size,
      artefact_id: Number(req.params.id),
    });

    const attachment = await nocodb.get('ArtefactAttachments', created.Id);
    res.status(201).json({ ...attachment, url: `/api/uploads/${attachment.filename}` });
  } catch (err) {
    console.error('Upload artefact attachment error:', err);
    res.status(500).json({ error: 'Failed to upload attachment' });
  }
});

app.delete('/api/artefact-attachments/:id', requireAuth, async (req, res) => {
  try {
    const attachment = await nocodb.get('ArtefactAttachments', req.params.id);
    if (!attachment) return res.status(404).json({ error: 'Attachment not found' });

    // Permission check via artefact
    const artefact = await nocodb.get('Artefacts', attachment.artefact_id);
    if (req.session.userRole === 'contributor' && artefact.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only manage attachments on your own artefacts' });
    }

    const filePath = path.join(uploadsDir, attachment.filename);
    if (fs.existsSync(filePath)) fs.unlinkSync(filePath);

    await nocodb.delete('ArtefactAttachments', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete artefact attachment error:', err);
    res.status(500).json({ error: 'Failed to delete attachment' });
  }
});

// ─── PUBLIC ARTEFACT REVIEW (no auth) ──────────────────────────

app.get('/api/public/review/:token', async (req, res) => {
  try {
    const artefacts = await nocodb.list('Artefacts', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });

    if (artefacts.length === 0) {
      return res.status(404).json({ error: 'Review link not found or expired' });
    }

    const artefact = artefacts[0];

    // Check expiration
    if (artefact.token_expires_at) {
      const expiresAt = new Date(artefact.token_expires_at);
      if (expiresAt < new Date()) {
        return res.status(410).json({ error: 'Review link has expired' });
      }
    }

    // Get the review version (or fall back to current/latest)
    const reviewVersionNumber = artefact.review_version || artefact.current_version || 1;
    
    // Get the specific version
    const versions = await nocodb.list('ArtefactVersions', {
      where: `(artefact_id,eq,${artefact.Id})~and(version_number,eq,${reviewVersionNumber})`,
      limit: 1,
    });

    let texts = [];
    let attachments = [];
    let versionData = null;

    if (versions.length > 0) {
      const version = versions[0];
      versionData = version;

      // Get texts and attachments for this version
      [texts, attachments] = await Promise.all([
        nocodb.list('ArtefactVersionTexts', {
          where: `(version_id,eq,${version.Id})`,
          limit: QUERY_LIMITS.large,
        }),
        nocodb.list('ArtefactAttachments', {
          where: `(version_id,eq,${version.Id})`,
          limit: QUERY_LIMITS.large,
        }),
      ]);
    } else {
      // Legacy support: get all attachments if no version found
      attachments = await nocodb.list('ArtefactAttachments', {
        where: `(artefact_id,eq,${artefact.Id})`,
        limit: QUERY_LIMITS.large,
      });
    }

    // Get comments for this version
    const comments = await nocodb.list('Comments', {
      where: `(entity_type,eq,artefact)~and(entity_id,eq,${artefact.Id})~and(version_number,eq,${reviewVersionNumber})`,
      sort: 'CreatedAt',
      limit: QUERY_LIMITS.large,
    });

    // Resolve approver names
    const approvers = [];
    if (artefact.approver_ids) {
      for (const id of artefact.approver_ids.split(',').filter(Boolean)) {
        approvers.push({ id: Number(id), name: await getRecordName('Users', Number(id)) });
      }
    }

    res.json({
      ...artefact,
      brand_name: await getRecordName('Brands', artefact.brand_id),
      creator_name: await getRecordName('Users', artefact.created_by_user_id),
      approvers,
      version: versionData,
      version_number: reviewVersionNumber,
      texts,
      attachments: attachments.map(a => ({
        ...a,
        url: a.drive_url || `/api/uploads/${a.filename}`,
      })),
      comments,
    });
  } catch (err) {
    console.error('Public review fetch error:', err);
    res.status(500).json({ error: 'Failed to load artefact for review' });
  }
});

app.post('/api/public/review/:token/approve', async (req, res) => {
  const { approved_by_name } = req.body;
  try {
    const artefacts = await nocodb.list('Artefacts', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (artefacts.length === 0) {
      return res.status(404).json({ error: 'Review link not found' });
    }
    const artefact = artefacts[0];

    if (artefact.token_expires_at && new Date(artefact.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    await nocodb.update('Artefacts', artefact.Id, {
      status: 'approved',
      approved_by_name: approved_by_name || 'Anonymous',
      approved_at: new Date().toISOString(),
    });

    res.json({ success: true, message: 'Artefact approved successfully' });
    notify.notifyApproved({ type: 'artefact', record: artefact, approverName: approved_by_name });
  } catch (err) {
    console.error('Approve error:', err);
    res.status(500).json({ error: 'Failed to approve artefact' });
  }
});

app.post('/api/public/review/:token/reject', async (req, res) => {
  const { approved_by_name, feedback } = req.body;
  try {
    const artefacts = await nocodb.list('Artefacts', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (artefacts.length === 0) {
      return res.status(404).json({ error: 'Review link not found' });
    }
    const artefact = artefacts[0];

    if (artefact.token_expires_at && new Date(artefact.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    await nocodb.update('Artefacts', artefact.Id, {
      status: 'rejected',
      approved_by_name: approved_by_name || 'Anonymous',
      feedback: feedback || '',
    });

    res.json({ success: true, message: 'Artefact rejected' });
    notify.notifyRejected({ type: 'artefact', record: artefact, approverName: approved_by_name, feedback });
  } catch (err) {
    console.error('Reject error:', err);
    res.status(500).json({ error: 'Failed to reject artefact' });
  }
});

app.post('/api/public/review/:token/revision', async (req, res) => {
  const { feedback, approved_by_name } = req.body;
  try {
    const artefacts = await nocodb.list('Artefacts', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (artefacts.length === 0) {
      return res.status(404).json({ error: 'Review link not found' });
    }
    const artefact = artefacts[0];

    if (artefact.token_expires_at && new Date(artefact.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    await nocodb.update('Artefacts', artefact.Id, {
      status: 'revision_requested',
      approved_by_name: approved_by_name || '',
      feedback: feedback || '',
    });

    res.json({ success: true, message: 'Revision requested' });
    notify.notifyRevisionRequested({ record: artefact, approverName: approved_by_name, feedback });
  } catch (err) {
    console.error('Revision request error:', err);
    res.status(500).json({ error: 'Failed to request revision' });
  }
});

app.post('/api/public/review/:token/comment', async (req, res) => {
  const { comment, author_name } = req.body;
  if (!comment) return res.status(400).json({ error: 'Comment is required' });

  try {
    const artefacts = await nocodb.list('Artefacts', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (artefacts.length === 0) {
      return res.status(404).json({ error: 'Review link not found' });
    }
    const artefact = artefacts[0];

    // Store comment in Comments table (assuming it exists with entity_type/entity_id pattern)
    try {
      await nocodb.create('Comments', {
        content: comment,
        author_name: author_name || 'Anonymous',
        entity_type: 'artefact',
        entity_id: artefact.Id,
      });
    } catch (err) {
      console.log('Comments table not available, skipping comment storage');
    }

    res.json({ success: true, message: 'Comment added' });
  } catch (err) {
    console.error('Comment error:', err);
    res.status(500).json({ error: 'Failed to add comment' });
  }
});

// ─── TRANSLATION MANAGEMENT API ──────────────────────────────────

// List translations
app.get('/api/translations', requireAuth, async (req, res) => {
  try {
    const { brand, status } = req.query;
    const whereParts = [];
    if (brand) whereParts.push(`(brand_id,eq,${sanitizeWhereValue(brand)})`);
    if (status) whereParts.push(`(status,eq,${sanitizeWhereValue(status)})`);
    const where = whereParts.length > 0 ? whereParts.join('~and') : undefined;

    let translations = await nocodb.list('Translations', { where, sort: '-UpdatedAt', limit: QUERY_LIMITS.medium });

    // Visibility filtering
    const userId = req.session.userId;
    if (req.session.userRole === 'contributor') {
      translations = translations.filter(t => t.created_by_user_id === userId);
    }

    // Enrich with names
    const brandIds = new Set(), userIds = new Set(), postIds = new Set();
    for (const t of translations) {
      if (t.brand_id) brandIds.add(t.brand_id);
      if (t.post_id) postIds.add(t.post_id);
      if (t.created_by_user_id) userIds.add(t.created_by_user_id);
      if (t.approver_ids) {
        for (const id of t.approver_ids.split(',').map(s => s.trim()).filter(Boolean)) {
          userIds.add(Number(id));
        }
      }
    }
    const names = await batchResolveNames({
      brand: { table: 'Brands', ids: [...brandIds] },
      user: { table: 'Users', ids: [...userIds] },
      post: { table: 'Posts', ids: [...postIds] },
    });

    // Count translation texts per record
    const textCounts = {};
    try {
      const allTexts = await nocodb.list('TranslationTexts', { limit: QUERY_LIMITS.large });
      for (const tt of allTexts) {
        textCounts[tt.translation_id] = (textCounts[tt.translation_id] || 0) + 1;
      }
    } catch (e) { /* table may not exist yet */ }

    res.json(translations.map(t => {
      const approverIdList = t.approver_ids ? t.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
      return {
        ...t,
        brand_name: names[`brand:${t.brand_id}`] || null,
        post_name: names[`post:${t.post_id}`] || null,
        creator_name: names[`user:${t.created_by_user_id}`] || null,
        approvers: approverIdList.map(id => ({ id: Number(id), name: names[`user:${Number(id)}`] || null })),
        translation_count: textCounts[t.Id] || 0,
      };
    }));
  } catch (err) {
    console.error('GET /translations error:', err);
    res.status(500).json({ error: 'Failed to load translations' });
  }
});

// Create translation
app.post('/api/translations', requireAuth, async (req, res) => {
  const { title, source_language, source_content, brand_id, post_id, approver_ids } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });
  if (!source_language) return res.status(400).json({ error: 'Source language is required' });
  if (!source_content) return res.status(400).json({ error: 'Source content is required' });

  try {
    const created = await nocodb.create('Translations', {
      title,
      source_language,
      source_content,
      status: 'draft',
      brand_id: brand_id ? Number(brand_id) : null,
      post_id: post_id ? Number(post_id) : null,
      approver_ids: approver_ids || null,
      created_by_user_id: req.session.userId,
    });

    const record = await nocodb.get('Translations', created.Id);
    const approverIdList = record.approver_ids ? record.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
    const approvers = [];
    for (const id of approverIdList) {
      approvers.push({ id: Number(id), name: await getRecordName('Users', Number(id)) });
    }
    res.status(201).json({
      ...record,
      brand_name: await getRecordName('Brands', record.brand_id),
      post_name: await getRecordName('Posts', record.post_id),
      creator_name: await getRecordName('Users', record.created_by_user_id),
      approvers,
      translation_count: 0,
    });
  } catch (err) {
    console.error('Create translation error:', err);
    res.status(500).json({ error: 'Failed to create translation' });
  }
});

// Bulk delete translations (BEFORE /:id)
app.post('/api/translations/bulk-delete', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const { ids } = req.body;
    if (!Array.isArray(ids) || ids.length === 0) return res.status(400).json({ error: 'ids array required' });
    for (const id of ids) {
      const texts = await nocodb.list('TranslationTexts', { where: `(translation_id,eq,${sanitizeWhereValue(id)})`, limit: QUERY_LIMITS.large });
      if (texts.length > 0) await nocodb.bulkDelete('TranslationTexts', texts.map(t => ({ Id: t.Id })));
    }
    await nocodb.bulkDelete('Translations', ids.map(id => ({ Id: id })));
    res.json({ deleted: ids.length });
  } catch (err) {
    console.error('Bulk delete translations error:', err);
    res.status(500).json({ error: 'Failed to bulk delete translations' });
  }
});

// Update translation
app.patch('/api/translations/:id', requireAuth, async (req, res) => {
  try {
    const existing = await nocodb.get('Translations', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Translation not found' });

    if (req.session.userRole === 'contributor' && existing.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only modify your own translations' });
    }

    const data = {};
    for (const f of ['title', 'source_language', 'source_content', 'status', 'feedback']) {
      if (req.body[f] !== undefined) data[f] = req.body[f];
    }
    if (req.body.brand_id !== undefined) data.brand_id = req.body.brand_id ? Number(req.body.brand_id) : null;
    if (req.body.post_id !== undefined) data.post_id = req.body.post_id ? Number(req.body.post_id) : null;
    if (req.body.approver_ids !== undefined) data.approver_ids = req.body.approver_ids || null;

    if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });

    await nocodb.update('Translations', req.params.id, data);

    const record = await nocodb.get('Translations', req.params.id);
    const approverIdList = record.approver_ids ? record.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
    const approvers = [];
    for (const id of approverIdList) {
      approvers.push({ id: Number(id), name: await getRecordName('Users', Number(id)) });
    }
    res.json({
      ...record,
      brand_name: await getRecordName('Brands', record.brand_id),
      post_name: await getRecordName('Posts', record.post_id),
      creator_name: await getRecordName('Users', record.created_by_user_id),
      approvers,
    });
  } catch (err) {
    console.error('Update translation error:', err);
    res.status(500).json({ error: 'Failed to update translation' });
  }
});

// Delete translation
app.delete('/api/translations/:id', requireAuth, async (req, res) => {
  try {
    const existing = await nocodb.get('Translations', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Translation not found' });

    if (req.session.userRole === 'contributor' && existing.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only delete your own translations' });
    }

    // Cascade delete translation texts
    const texts = await nocodb.list('TranslationTexts', { where: `(translation_id,eq,${sanitizeWhereValue(req.params.id)})`, limit: QUERY_LIMITS.large });
    if (texts.length > 0) await nocodb.bulkDelete('TranslationTexts', texts.map(t => ({ Id: t.Id })));

    await nocodb.delete('Translations', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete translation error:', err);
    res.status(500).json({ error: 'Failed to delete translation' });
  }
});

// List translation texts
app.get('/api/translations/:id/texts', requireAuth, async (req, res) => {
  try {
    const texts = await nocodb.list('TranslationTexts', {
      where: `(translation_id,eq,${sanitizeWhereValue(req.params.id)})`,
      limit: QUERY_LIMITS.large,
    });
    res.json(texts);
  } catch (err) {
    console.error('GET translation texts error:', err);
    res.status(500).json({ error: 'Failed to load translation texts' });
  }
});

// Add/update translation text
app.post('/api/translations/:id/texts', requireAuth, async (req, res) => {
  const { language_code, language_label, content } = req.body;
  if (!language_code || !content) return res.status(400).json({ error: 'Language code and content are required' });

  try {
    const translation = await nocodb.get('Translations', req.params.id);
    if (!translation) return res.status(404).json({ error: 'Translation not found' });

    // Count existing options for this language to assign option_number
    const existing = await nocodb.list('TranslationTexts', {
      where: `(translation_id,eq,${sanitizeWhereValue(req.params.id)})~and(language_code,eq,${sanitizeWhereValue(language_code)})`,
      limit: QUERY_LIMITS.large,
    });
    const optionNumber = existing.length + 1;

    const result = await nocodb.create('TranslationTexts', {
      translation_id: Number(req.params.id),
      language_code,
      language_label: language_label || language_code,
      content,
      option_number: optionNumber,
      is_selected: false,
    });

    res.json(result);
  } catch (err) {
    console.error('Add translation text error:', err);
    res.status(500).json({ error: 'Failed to save translation text' });
  }
});

// Update translation text content
app.patch('/api/translations/:id/texts/:textId', requireAuth, async (req, res) => {
  const { content } = req.body;
  if (!content) return res.status(400).json({ error: 'Content is required' });
  try {
    await nocodb.update('TranslationTexts', req.params.textId, { content });
    const result = await nocodb.get('TranslationTexts', req.params.textId);
    res.json(result);
  } catch (err) {
    console.error('Update translation text error:', err);
    res.status(500).json({ error: 'Failed to update translation text' });
  }
});

// Delete translation text
app.delete('/api/translations/:id/texts/:textId', requireAuth, async (req, res) => {
  try {
    await nocodb.delete('TranslationTexts', req.params.textId);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete translation text error:', err);
    res.status(500).json({ error: 'Failed to delete translation text' });
  }
});

// Select a translation option (used by approver via public review)
app.post('/api/public/review-translation/:token/select', async (req, res) => {
  const { text_id } = req.body;
  if (!text_id) return res.status(400).json({ error: 'text_id is required' });

  try {
    const translations = await nocodb.list('Translations', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (translations.length === 0) return res.status(404).json({ error: 'Review link not found' });
    const translation = translations[0];

    if (translation.token_expires_at && new Date(translation.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    // Get the text being selected to find its language
    const textToSelect = await nocodb.get('TranslationTexts', text_id);
    if (!textToSelect || textToSelect.translation_id !== translation.Id) {
      return res.status(400).json({ error: 'Invalid text selection' });
    }

    // Deselect all options for this language, then select the chosen one
    const sameLanguage = await nocodb.list('TranslationTexts', {
      where: `(translation_id,eq,${translation.Id})~and(language_code,eq,${sanitizeWhereValue(textToSelect.language_code)})`,
      limit: QUERY_LIMITS.large,
    });
    for (const t of sameLanguage) {
      await nocodb.update('TranslationTexts', t.Id, { is_selected: t.Id === Number(text_id) });
    }

    res.json({ success: true });
  } catch (err) {
    console.error('Select translation error:', err);
    res.status(500).json({ error: 'Failed to select translation' });
  }
});

// Public: Approver suggests a new/modified translation option
app.post('/api/public/review-translation/:token/suggest', async (req, res) => {
  const { language_code, language_label, content, suggested_by } = req.body;
  if (!language_code || !content) return res.status(400).json({ error: 'Language code and content are required' });

  try {
    const translations = await nocodb.list('Translations', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (translations.length === 0) return res.status(404).json({ error: 'Review link not found' });
    const translation = translations[0];

    if (translation.token_expires_at && new Date(translation.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    // Count existing options for this language
    const existing = await nocodb.list('TranslationTexts', {
      where: `(translation_id,eq,${translation.Id})~and(language_code,eq,${sanitizeWhereValue(language_code)})`,
      limit: QUERY_LIMITS.large,
    });
    const optionNumber = existing.length + 1;

    const result = await nocodb.create('TranslationTexts', {
      translation_id: translation.Id,
      language_code,
      language_label: language_label || language_code,
      content: suggested_by ? `[Suggested by ${suggested_by}] ${content}` : content,
      option_number: optionNumber,
      is_selected: false,
    });

    res.json(result);
  } catch (err) {
    console.error('Suggest translation error:', err);
    res.status(500).json({ error: 'Failed to add suggestion' });
  }
});

// Submit translation for review
app.post('/api/translations/:id/submit-review', requireAuth, async (req, res) => {
  try {
    const existing = await nocodb.get('Translations', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Translation not found' });

    if (req.session.userRole === 'contributor' && existing.created_by_user_id !== req.session.userId) {
      return res.status(403).json({ error: 'You can only submit your own translations' });
    }

    const token = require('crypto').randomUUID();
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + DEFAULTS.tokenExpiryDays);

    await nocodb.update('Translations', req.params.id, {
      status: 'pending_review',
      approval_token: token,
      token_expires_at: expiresAt.toISOString(),
    });

    const reviewUrl = `${req.protocol}://${req.get('host')}/review-translation/${token}`;
    res.json({ success: true, token, reviewUrl, expiresAt: expiresAt.toISOString() });
    notify.notifyReviewSubmitted({ type: 'translation', record: existing, reviewUrl });
  } catch (err) {
    console.error('Submit translation review error:', err);
    res.status(500).json({ error: 'Failed to submit for review' });
  }
});

// Public: Get translation for review
app.get('/api/public/review-translation/:token', async (req, res) => {
  try {
    const translations = await nocodb.list('Translations', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });

    if (translations.length === 0) {
      return res.status(404).json({ error: 'Review link not found or expired' });
    }

    const translation = translations[0];

    if (translation.token_expires_at) {
      const expiresAt = new Date(translation.token_expires_at);
      if (expiresAt < new Date()) {
        return res.status(410).json({ error: 'Review link has expired' });
      }
    }

    // Get all translation texts
    const texts = await nocodb.list('TranslationTexts', {
      where: `(translation_id,eq,${translation.Id})`,
      limit: QUERY_LIMITS.large,
    });

    // Resolve approver names
    const approvers = [];
    if (translation.approver_ids) {
      for (const id of translation.approver_ids.split(',').filter(Boolean)) {
        approvers.push({ id: Number(id), name: await getRecordName('Users', Number(id)) });
      }
    }

    res.json({
      ...translation,
      brand_name: await getRecordName('Brands', translation.brand_id),
      creator_name: await getRecordName('Users', translation.created_by_user_id),
      approvers,
      texts,
    });
  } catch (err) {
    console.error('Public translation review fetch error:', err);
    res.status(500).json({ error: 'Failed to load translation for review' });
  }
});

// Public: Approve translation
app.post('/api/public/review-translation/:token/approve', async (req, res) => {
  const { approved_by_name } = req.body;
  try {
    const translations = await nocodb.list('Translations', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (translations.length === 0) return res.status(404).json({ error: 'Review link not found' });
    const translation = translations[0];

    if (translation.token_expires_at && new Date(translation.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    await nocodb.update('Translations', translation.Id, {
      status: 'approved',
      approved_by_name: approved_by_name || 'Anonymous',
      approved_at: new Date().toISOString(),
    });

    res.json({ success: true, message: 'Translation approved successfully' });
    notify.notifyApproved({ type: 'translation', record: translation, approverName: approved_by_name });
  } catch (err) {
    console.error('Approve translation error:', err);
    res.status(500).json({ error: 'Failed to approve translation' });
  }
});

// Public: Reject translation
app.post('/api/public/review-translation/:token/reject', async (req, res) => {
  const { approved_by_name, feedback } = req.body;
  try {
    const translations = await nocodb.list('Translations', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (translations.length === 0) return res.status(404).json({ error: 'Review link not found' });
    const translation = translations[0];

    if (translation.token_expires_at && new Date(translation.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    await nocodb.update('Translations', translation.Id, {
      status: 'rejected',
      approved_by_name: approved_by_name || 'Anonymous',
      feedback: feedback || '',
    });

    res.json({ success: true, message: 'Translation rejected' });
    notify.notifyRejected({ type: 'translation', record: translation, approverName: approved_by_name, feedback });
  } catch (err) {
    console.error('Reject translation error:', err);
    res.status(500).json({ error: 'Failed to reject translation' });
  }
});

// Public: Request revision on translation
app.post('/api/public/review-translation/:token/revision', async (req, res) => {
  const { feedback, approved_by_name } = req.body;
  try {
    const translations = await nocodb.list('Translations', {
      where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (translations.length === 0) return res.status(404).json({ error: 'Review link not found' });
    const translation = translations[0];

    if (translation.token_expires_at && new Date(translation.token_expires_at) < new Date()) {
      return res.status(410).json({ error: 'Review link has expired' });
    }

    await nocodb.update('Translations', translation.Id, {
      status: 'revision_requested',
      approved_by_name: approved_by_name || '',
      feedback: feedback || '',
    });

    res.json({ success: true, message: 'Revision requested' });
    notify.notifyRevisionRequested({ record: translation, approverName: approved_by_name, feedback });
  } catch (err) {
    console.error('Translation revision request error:', err);
    res.status(500).json({ error: 'Failed to request revision' });
  }
});

// ─── ISSUE TRACKER API ──────────────────────────────────────────

// Internal: List issues with filters
app.get('/api/issues', requireAuth, async (req, res) => {
  try {
    const { status, category, type, priority, assigned_to_id, brand_id, team_id, sort } = req.query;
    const conditions = [];
    if (status) conditions.push({ field: 'status', op: 'eq', value: sanitizeWhereValue(status) });
    if (category) conditions.push({ field: 'category', op: 'eq', value: sanitizeWhereValue(category) });
    if (type) conditions.push({ field: 'type', op: 'eq', value: sanitizeWhereValue(type) });
    if (priority) conditions.push({ field: 'priority', op: 'eq', value: sanitizeWhereValue(priority) });
    if (assigned_to_id) conditions.push({ field: 'assigned_to_id', op: 'eq', value: sanitizeWhereValue(assigned_to_id) });
    if (brand_id) conditions.push({ field: 'brand_id', op: 'eq', value: sanitizeWhereValue(brand_id) });
    if (team_id) conditions.push({ field: 'team_id', op: 'eq', value: sanitizeWhereValue(team_id) });

    let issues = await nocodb.list('Issues', {
      where: conditions,
      sort: sort || '-created_at',
    });

    // Team-based visibility filtering
    const userId = req.session.userId;
    if (req.session.userRole === 'manager') {
      const myTeamIds = await getUserTeamIds(userId);
      issues = issues.filter(i =>
        i.assigned_to_id === userId ||
        (i.team_id && myTeamIds.has(i.team_id)) || !i.team_id
      );
    } else if (req.session.userRole === 'contributor') {
      issues = issues.filter(i => i.assigned_to_id === userId);
    }

    // Resolve brand and team names
    const names = await batchResolveNames({
      brand: { table: 'Brands', ids: issues.map(i => i.brand_id) },
      team: { table: 'Teams', ids: issues.map(i => i.team_id) },
    });
    for (const issue of issues) {
      issue.brand_name = names[`brand:${issue.brand_id}`] || null;
      issue.team_name = names[`team:${issue.team_id}`] || null;
      issue.thumbnail_url = issue.thumbnail || null;
    }

    // Count by status for dashboard
    const counts = { new: 0, acknowledged: 0, in_progress: 0, resolved: 0, declined: 0 };
    issues.forEach(i => counts[i.status] = (counts[i.status] || 0) + 1);

    res.json({ issues, counts });
  } catch (err) {
    console.error('List issues error:', err);
    res.status(500).json({ error: 'Failed to load issues' });
  }
});

// Internal: List distinct categories
app.get('/api/issues/categories', requireAuth, async (req, res) => {
  try {
    const issues = await nocodb.list('Issues', { fields: 'category' });
    const categories = [...new Set(issues.map(i => i.category).filter(Boolean))];
    res.json(categories);
  } catch (err) {
    console.error('Get categories error:', err);
    res.status(500).json({ error: 'Failed to load categories' });
  }
});

// Bulk delete issues
app.post('/api/issues/bulk-delete', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    const { ids } = req.body;
    if (!Array.isArray(ids) || ids.length === 0) return res.status(400).json({ error: 'ids array required' });
    for (const id of ids) {
      const updates = await nocodb.list('IssueUpdates', { where: `(issue_id,eq,${sanitizeWhereValue(id)})`, limit: QUERY_LIMITS.large });
      if (updates.length > 0) await nocodb.bulkDelete('IssueUpdates', updates.map(u => ({ Id: u.Id })));
      const atts = await nocodb.list('IssueAttachments', { where: `(issue_id,eq,${sanitizeWhereValue(id)})`, limit: QUERY_LIMITS.large });
      if (atts.length > 0) await nocodb.bulkDelete('IssueAttachments', atts.map(a => ({ Id: a.Id })));
    }
    await nocodb.bulkDelete('Issues', ids.map(id => ({ Id: id })));
    res.json({ deleted: ids.length });
  } catch (err) {
    console.error('Bulk delete issues error:', err);
    res.status(500).json({ error: 'Failed to bulk delete issues' });
  }
});

// Internal: Get single issue with updates and attachments
app.get('/api/issues/:id', requireAuth, async (req, res) => {
  try {
    const issue = await nocodb.get('Issues', req.params.id);
    if (!issue) return res.status(404).json({ error: 'Issue not found' });

    const [updates, attachments] = await Promise.all([
      nocodb.list('IssueUpdates', { where: `(issue_id,eq,${sanitizeWhereValue(req.params.id)})`, sort: '-created_at' }),
      nocodb.list('IssueAttachments', { where: `(issue_id,eq,${sanitizeWhereValue(req.params.id)})`, sort: '-created_at' }),
    ]);

    let brand_name = null;
    if (issue.brand_id) {
      try { brand_name = (await nocodb.get('Brands', issue.brand_id)).name; } catch (err) { console.error('Resolve issue brand:', err.message); }
    }
    let team_name = null;
    if (issue.team_id) {
      try { team_name = (await nocodb.get('Teams', issue.team_id)).name; } catch (err) { console.error('Resolve issue team:', err.message); }
    }

    res.json({ ...issue, brand_name, team_name, updates, attachments });
  } catch (err) {
    console.error('Get issue error:', err);
    res.status(500).json({ error: 'Failed to load issue' });
  }
});

// Internal: Create issue (on behalf of someone)
app.post('/api/issues', requireAuth, async (req, res) => {
  try {
    const { title, description, type, category, priority, submitter_name, submitter_email, submitter_phone } = req.body;
    if (!title || !submitter_name || !submitter_email) {
      return res.status(400).json({ error: 'Title, submitter name, and email are required' });
    }

    const now = new Date().toISOString();
    const issue = await nocodb.create('Issues', {
      title,
      description: description || '',
      type: type || 'request',
      category: category || 'General',
      priority: priority || 'medium',
      status: 'new',
      submitter_name,
      submitter_email,
      submitter_phone: submitter_phone || '',
      tracking_token: require('crypto').randomUUID(),
      created_at: now,
      updated_at: now,
    });

    res.json(issue);
  } catch (err) {
    console.error('Create issue error:', err);
    res.status(500).json({ error: 'Failed to create issue' });
  }
});

// Internal: Update issue
app.patch('/api/issues/:id', requireAuth, async (req, res) => {
  try {
    const existing = await nocodb.get('Issues', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Issue not found' });

    const { status, assigned_to_id, internal_notes, resolution_summary, priority, category, brand_id, team_id } = req.body;
    const updates = { updated_at: new Date().toISOString() };

    if (status !== undefined) updates.status = status;
    if (assigned_to_id !== undefined) updates.assigned_to_id = assigned_to_id;
    if (internal_notes !== undefined) updates.internal_notes = internal_notes;
    if (resolution_summary !== undefined) updates.resolution_summary = resolution_summary;
    if (priority !== undefined) updates.priority = priority;
    if (category !== undefined) updates.category = category;
    if (brand_id !== undefined) updates.brand_id = brand_id ? Number(brand_id) : null;
    if (team_id !== undefined) updates.team_id = team_id ? Number(team_id) : null;

    if (status === 'resolved' || status === 'declined') {
      updates.resolved_at = new Date().toISOString();
    }

    await nocodb.update('Issues', req.params.id, updates);
    res.json({ success: true });
    // Notify on assignment change
    if (assigned_to_id && assigned_to_id !== existing.assigned_to_id) {
      notify.notifyIssueAssigned({ issue: { ...existing, ...updates }, assignerName: req.session.userName });
    }
    // Notify submitter on status change
    if (status && status !== existing.status) {
      notify.notifyIssueStatusUpdate({ issue: { ...existing, ...updates }, oldStatus: existing.status, newStatus: status });
    }
  } catch (err) {
    console.error('Update issue error:', err);
    res.status(500).json({ error: 'Failed to update issue' });
  }
});

// Internal: Delete issue
app.delete('/api/issues/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
  try {
    await nocodb.delete('Issues', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete issue error:', err);
    res.status(500).json({ error: 'Failed to delete issue' });
  }
});

// Internal: Add update to issue
app.post('/api/issues/:id/updates', requireAuth, async (req, res) => {
  try {
    const { message, is_public } = req.body;
    if (!message) return res.status(400).json({ error: 'Message is required' });

    const update = await nocodb.create('IssueUpdates', {
      issue_id: Number(req.params.id),
      message,
      is_public: is_public || false,
      author_name: req.session.userName || 'Staff',
      author_type: 'staff',
      created_at: new Date().toISOString(),
    });

    await nocodb.update('Issues', req.params.id, { updated_at: new Date().toISOString() });
    res.json(update);
  } catch (err) {
    console.error('Add update error:', err);
    res.status(500).json({ error: 'Failed to add update' });
  }
});

// Internal: Upload attachment
app.post('/api/issues/:id/attachments', requireAuth, dynamicUpload('file'), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ error: 'No file uploaded' });

    const attachment = await nocodb.create('IssueAttachments', {
      issue_id: Number(req.params.id),
      filename: req.file.filename,
      original_name: req.file.originalname,
      mime_type: req.file.mimetype,
      size: req.file.size,
      uploaded_by: req.session.userName || 'Staff',
      created_at: new Date().toISOString(),
    });

    res.json(attachment);
  } catch (err) {
    console.error('Upload attachment error:', err);
    res.status(500).json({ error: 'Failed to upload attachment' });
  }
});

// Internal: Get attachments
app.get('/api/issues/:id/attachments', requireAuth, async (req, res) => {
  try {
    const attachments = await nocodb.list('IssueAttachments', {
      where: `(issue_id,eq,${sanitizeWhereValue(req.params.id)})`,
      sort: '-created_at',
    });
    res.json(attachments);
  } catch (err) {
    console.error('Get attachments error:', err);
    res.status(500).json({ error: 'Failed to load attachments' });
  }
});

// Internal: Delete attachment
app.delete('/api/issue-attachments/:id', requireAuth, async (req, res) => {
  try {
    const attachment = await nocodb.get('IssueAttachments', req.params.id);
    if (attachment && attachment.filename) {
      const filePath = path.join(uploadsDir, attachment.filename);
      if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
    }
    await nocodb.delete('IssueAttachments', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete attachment error:', err);
    res.status(500).json({ error: 'Failed to delete attachment' });
  }
});

// Set an issue's thumbnail from one of its image attachments
app.patch('/api/issues/:id/thumbnail', requireAuth, async (req, res) => {
  try {
    const { attachment_id } = req.body;
    const issue = await nocodb.get('Issues', req.params.id);
    if (!issue) return res.status(404).json({ error: 'Issue not found' });

    if (attachment_id) {
      const att = await nocodb.get('IssueAttachments', attachment_id);
      if (!att) return res.status(404).json({ error: 'Attachment not found' });
      await nocodb.update('Issues', req.params.id, { thumbnail: att.url || `/api/uploads/${att.filename}` });
    } else {
      await nocodb.update('Issues', req.params.id, { thumbnail: null });
    }

    const updated = await nocodb.get('Issues', req.params.id);
    res.json(updated);
  } catch (err) {
    res.status(500).json({ error: 'Failed to set thumbnail' });
  }
});

// ─── PUBLIC ISSUE ROUTES (NO AUTH) ──────────────────────────────

// Public: List teams for issue submission
app.get('/api/public/teams', async (req, res) => {
  try {
    const teams = await nocodb.list('Teams', { sort: 'name', limit: QUERY_LIMITS.medium, fields: 'Id,name' });
    res.json(teams.map(t => ({ id: t.Id, name: t.name })));
  } catch (err) {
    console.error('Public teams error:', err);
    res.status(500).json({ error: 'Failed to load teams' });
  }
});

// Public: Submit new issue
app.post('/api/public/issues', dynamicUpload('file'), async (req, res) => {
  try {
    const { name, email, phone, title, description, type, category, priority, team_id } = req.body;
    if (!name || !email || !title) {
      return res.status(400).json({ error: 'Name, email, and title are required' });
    }

    const now = new Date().toISOString();
    const token = require('crypto').randomUUID();

    const issue = await nocodb.create('Issues', {
      title,
      description: description || '',
      type: type || 'request',
      category: category || 'General',
      priority: priority || 'medium',
      status: 'new',
      submitter_name: name,
      submitter_email: email,
      submitter_phone: phone || '',
      tracking_token: token,
      team_id: team_id ? Number(team_id) : null,
      created_at: now,
      updated_at: now,
    });

    // Upload attachment if provided
    if (req.file) {
      await nocodb.create('IssueAttachments', {
        issue_id: issue.Id,
        filename: req.file.filename,
        original_name: req.file.originalname,
        mime_type: req.file.mimetype,
        size: req.file.size,
        uploaded_by: name,
        created_at: now,
      });
    }

    res.json({ success: true, token, trackingUrl: `/track/${token}` });
  } catch (err) {
    console.error('Public submit error:', err);
    res.status(500).json({ error: 'Failed to submit issue' });
  }
});

// Public: Get issue status by token
app.get('/api/public/issues/:token', async (req, res) => {
  try {
    const issues = await nocodb.list('Issues', {
      where: `(tracking_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (issues.length === 0) return res.status(404).json({ error: 'Issue not found' });

    const issue = issues[0];
    const [updates, attachments] = await Promise.all([
      nocodb.list('IssueUpdates', {
        where: `(issue_id,eq,${issue.Id})~and(is_public,eq,true)`,
        sort: '-created_at',
      }),
      nocodb.list('IssueAttachments', {
        where: `(issue_id,eq,${issue.Id})`,
        sort: '-created_at',
      }),
    ]);

    // Don't expose internal notes
    const publicIssue = {
      title: issue.title,
      description: issue.description,
      type: issue.type,
      category: issue.category,
      priority: issue.priority,
      status: issue.status,
      resolution_summary: issue.resolution_summary,
      created_at: issue.created_at,
      updated_at: issue.updated_at,
      resolved_at: issue.resolved_at,
    };

    res.json({ issue: publicIssue, updates, attachments });
  } catch (err) {
    console.error('Public get issue error:', err);
    res.status(500).json({ error: 'Failed to load issue' });
  }
});

// Public: Add comment to issue
app.post('/api/public/issues/:token/comment', async (req, res) => {
  try {
    const { name, message } = req.body;
    if (!message) return res.status(400).json({ error: 'Message is required' });

    const issues = await nocodb.list('Issues', {
      where: `(tracking_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (issues.length === 0) return res.status(404).json({ error: 'Issue not found' });

    const update = await nocodb.create('IssueUpdates', {
      issue_id: issues[0].Id,
      message,
      is_public: true,
      author_name: name || 'Submitter',
      author_type: 'submitter',
      created_at: new Date().toISOString(),
    });

    await nocodb.update('Issues', issues[0].Id, { updated_at: new Date().toISOString() });
    res.json(update);
  } catch (err) {
    console.error('Public comment error:', err);
    res.status(500).json({ error: 'Failed to add comment' });
  }
});

// Public: Upload attachment to issue
app.post('/api/public/issues/:token/attachments', dynamicUpload('file'), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ error: 'No file uploaded' });

    const issues = await nocodb.list('Issues', {
      where: `(tracking_token,eq,${sanitizeWhereValue(req.params.token)})`,
      limit: 1,
    });
    if (issues.length === 0) return res.status(404).json({ error: 'Issue not found' });

    const attachment = await nocodb.create('IssueAttachments', {
      issue_id: issues[0].Id,
      filename: req.file.filename,
      original_name: req.file.originalname,
      mime_type: req.file.mimetype,
      size: req.file.size,
      uploaded_by: req.body.name || 'Submitter',
      created_at: new Date().toISOString(),
    });

    res.json(attachment);
  } catch (err) {
    console.error('Public upload error:', err);
    res.status(500).json({ error: 'Failed to upload attachment' });
  }
});

app.use((err, req, res, next) => {
  console.error(`[ERROR] ${req.method} ${req.path}:`, err.message);
  res.status(500).json({ error: 'Internal server error', details: err.message });
});

process.on('uncaughtException', (err) => {
  console.error('[UNCAUGHT]', err.message);
});
process.on('unhandledRejection', (err) => {
  console.error('[UNHANDLED REJECTION]', err);
});

// ─── ROLES ──────────────────────────────────────────────────────

app.get('/api/roles', requireAuth, async (req, res) => {
  try {
    const roles = await nocodb.list('Roles', { sort: 'name', limit: QUERY_LIMITS.medium });
    res.json(roles.map(r => ({ ...r, id: r.Id, _id: r.Id })));
  } catch (err) {
    console.error('Roles list error:', err);
    res.status(500).json({ error: 'Failed to load roles' });
  }
});

app.post('/api/roles', requireAuth, requireRole('superadmin'), async (req, res) => {
  const { name, color } = req.body;
  if (!name) return res.status(400).json({ error: 'Name is required' });
  try {
    const created = await nocodb.create('Roles', { name, color: color || null });
    const role = await nocodb.get('Roles', created.Id);
    res.status(201).json({ ...role, id: role.Id, _id: role.Id });
  } catch (err) {
    console.error('Create role error:', err);
    res.status(500).json({ error: 'Failed to create role' });
  }
});

app.patch('/api/roles/:id', requireAuth, requireRole('superadmin'), async (req, res) => {
  try {
    const existing = await nocodb.get('Roles', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Role not found' });
    const data = {};
    if (req.body.name !== undefined) data.name = req.body.name;
    if (req.body.color !== undefined) data.color = req.body.color;
    if (Object.keys(data).length > 0) await nocodb.update('Roles', req.params.id, data);
    const role = await nocodb.get('Roles', req.params.id);
    res.json({ ...role, id: role.Id, _id: role.Id });
  } catch (err) {
    console.error('Update role error:', err);
    res.status(500).json({ error: 'Failed to update role' });
  }
});

app.delete('/api/roles/:id', requireAuth, requireRole('superadmin'), async (req, res) => {
  try {
    const existing = await nocodb.get('Roles', req.params.id);
    if (!existing) return res.status(404).json({ error: 'Role not found' });
    // Check if any users have this role
    const usersWithRole = await nocodb.list('Users', { where: `(role_id,eq,${sanitizeWhereValue(req.params.id)})`, limit: 1 });
    if (usersWithRole.length > 0) return res.status(409).json({ error: 'Cannot delete role that is assigned to users' });
    await nocodb.delete('Roles', req.params.id);
    res.json({ success: true });
  } catch (err) {
    console.error('Delete role error:', err);
    res.status(500).json({ error: 'Failed to delete role' });
  }
});

// ─── APP SETTINGS API ───────────────────────────────────────────

app.get('/api/settings/app', requireAuth, (req, res) => {
  res.json(appSettings);
});

app.patch('/api/settings/app', requireAuth, requireRole('superadmin'), (req, res) => {
  const { uploadMaxSizeMB } = req.body;
  if (uploadMaxSizeMB !== undefined) {
    const val = Number(uploadMaxSizeMB);
    if (isNaN(val) || val < 1 || val > 500) {
      return res.status(400).json({ error: 'uploadMaxSizeMB must be between 1 and 500' });
    }
    appSettings.uploadMaxSizeMB = val;
  }
  saveSettings(appSettings);
  res.json(appSettings);
});

// ─── AUTH MIGRATION (one-time: auth.db → NocoDB) ────────────────

async function migrateAuthToNocoDB() {
  const authDbPath = path.join(__dirname, 'auth.db');
  if (!fs.existsSync(authDbPath)) {
    console.log('  No auth.db found — skipping auth migration.');
    return;
  }

  let Database;
  try {
    Database = require('better-sqlite3');
  } catch {
    try {
      // Fallback: use sqlite3 CLI
      const { execSync } = require('child_process');
      const raw = execSync(`sqlite3 "${authDbPath}" "SELECT email, password_hash, nocodb_user_id FROM auth_credentials;"`, { encoding: 'utf8' });
      const rows = raw.trim().split('\n').filter(Boolean).map(line => {
        const [email, password_hash, nocodb_user_id] = line.split('|');
        return { email, password_hash, nocodb_user_id: Number(nocodb_user_id) };
      });
      if (rows.length === 0) { console.log('  auth.db is empty — nothing to migrate.'); return; }
      let migrated = 0, skipped = 0;
      for (const cred of rows) {
        try {
          const user = await nocodb.get('Users', cred.nocodb_user_id);
          if (!user) { console.warn(`  User ${cred.nocodb_user_id} (${cred.email}) not found in NocoDB — skipping.`); skipped++; continue; }
          if (user.password_hash) { skipped++; continue; }
          await nocodb.update('Users', cred.nocodb_user_id, { password_hash: cred.password_hash });
          migrated++;
        } catch (err) { console.error(`  Failed to migrate user ${cred.email}:`, err.message); }
      }
      console.log(`  Auth migration: ${migrated} migrated, ${skipped} skipped.`);
      if (migrated > 0) {
        const bakPath = authDbPath + '.bak';
        if (!fs.existsSync(bakPath)) { fs.renameSync(authDbPath, bakPath); console.log('  Renamed auth.db → auth.db.bak'); }
      }
      return;
    } catch (cliErr) {
      console.warn('  Cannot read auth.db (no better-sqlite3 or sqlite3 CLI):', cliErr.message);
      return;
    }
  }

  const db = new Database(authDbPath, { readonly: true });
  try {
    const creds = db.prepare('SELECT email, password_hash, nocodb_user_id FROM auth_credentials').all();
    if (creds.length === 0) { console.log('  auth.db is empty — nothing to migrate.'); return; }
    let migrated = 0, skipped = 0;
    for (const cred of creds) {
      try {
        const user = await nocodb.get('Users', cred.nocodb_user_id);
        if (!user) { console.warn(`  User ${cred.nocodb_user_id} (${cred.email}) not found in NocoDB — skipping.`); skipped++; continue; }
        if (user.password_hash) { skipped++; continue; }
        await nocodb.update('Users', cred.nocodb_user_id, { password_hash: cred.password_hash });
        migrated++;
      } catch (err) { console.error(`  Failed to migrate user ${cred.email}:`, err.message); }
    }
    console.log(`  Auth migration: ${migrated} migrated, ${skipped} skipped.`);
    if (migrated > 0) {
      const bakPath = authDbPath + '.bak';
      if (!fs.existsSync(bakPath)) { fs.renameSync(authDbPath, bakPath); console.log('  Renamed auth.db → auth.db.bak'); }
    }
  } finally {
    db.close();
  }
}

// ─── START SERVER ───────────────────────────────────────────────

async function startServer() {
  // Validate required env vars
  const REQUIRED_ENV = {
    NOCODB_URL: 'NocoDB base URL (e.g., http://localhost:8090)',
    NOCODB_TOKEN: 'NocoDB API token',
    NOCODB_BASE_ID: 'NocoDB base/project ID',
  };
  const OPTIONAL_ENV = {
    SESSION_SECRET: 'Session encryption secret (required in production)',
    APP_URL: 'Public app URL for email links',
    CLOUDRON_MAIL_SMTP_SERVER: 'SMTP server for password reset emails',
    CORS_ORIGIN: 'Allowed CORS origin',
  };

  let missingRequired = false;
  for (const [key, desc] of Object.entries(REQUIRED_ENV)) {
    if (!process.env[key]) {
      console.error(`MISSING required env var: ${key} — ${desc}`);
      missingRequired = true;
    }
  }
  if (missingRequired) {
    console.error('Cannot start server. Set required environment variables and retry.');
    console.error('See .env.example for a template.');
    process.exit(1);
  }
  for (const [key, desc] of Object.entries(OPTIONAL_ENV)) {
    if (!process.env[key]) console.warn(`  Optional env var not set: ${key} — ${desc}`);
  }

  console.log('Ensuring required tables...');
  await ensureRequiredTables();
  console.log('Running FK column migration...');
  await ensureFKColumns();
  await ensureTextColumns();
  await backfillFKs();
  console.log('Checking auth migration...');
  await migrateAuthToNocoDB();
  console.log('Migration complete.');

  // Verify critical columns exist (belt-and-suspenders check)
  try {
    const artTableId = await nocodb.resolveTableId('Artefacts');
    const artMeta = await fetch(`${nocodb.url}/api/v2/meta/tables/${artTableId}`, {
      headers: { 'xc-token': nocodb.token },
    }).then(r => r.json());
    const artCols = (artMeta.columns || []).map(c => c.title);
    console.log(`Artefacts table columns: ${artCols.join(', ')}`);
    for (const needed of ['approver_ids', 'project_id', 'campaign_id']) {
      if (!artCols.includes(needed)) {
        console.warn(`⚠ MISSING column Artefacts.${needed} — creating now...`);
        const uidt = needed === 'approver_ids' ? 'SingleLineText' : 'Number';
        const cr = await fetch(`${nocodb.url}/api/v2/meta/tables/${artTableId}/columns`, {
          method: 'POST',
          headers: { 'xc-token': nocodb.token, 'Content-Type': 'application/json' },
          body: JSON.stringify({ title: needed, uidt }),
        });
        console.log(`  → ${needed}: ${cr.ok ? 'CREATED' : 'FAILED ' + cr.status}`);
      }
    }
  } catch (err) {
    console.error('Column verification failed:', err.message);
  }

  // In production, serve the built frontend and handle SPA routing
  if (process.env.NODE_ENV === 'production') {
    const clientDist = path.join(__dirname, '..', 'client', 'dist');
    app.use(express.static(clientDist));

    // OG meta tags for public review links (WhatsApp, Slack, etc.)
    app.get('/review-post/:token', async (req, res) => {
      try {
        const indexHtml = require('fs').readFileSync(path.join(clientDist, 'index.html'), 'utf-8');
        const posts = await nocodb.list('Posts', {
          where: `(approval_token,eq,${sanitizeWhereValue(req.params.token)})`,
          limit: 1,
        });
        if (posts.length === 0) return res.send(indexHtml);
        const post = posts[0];

        // Find first image attachment for thumbnail
        let ogImage = '';
        try {
          const attachments = await nocodb.list('PostAttachments', {
            where: `(post_id,eq,${post.Id})`,
            limit: 10,
          });
          const img = attachments.find(a => (a.mime_type || '').startsWith('image/'));
          if (img) {
            const baseUrl = `${req.protocol}://${req.get('host')}`;
            ogImage = img.url?.startsWith('http') ? img.url : `${baseUrl}${img.url}`;
          }
        } catch (e) { /* no attachments */ }

        const title = post.title || 'Post Review';
        const desc = post.description ? post.description.slice(0, 200) : 'Review and approve this post';
        const escape = s => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');

        const ogTags = `
    <meta property="og:title" content="${escape(title)}" />
    <meta property="og:description" content="${escape(desc)}" />
    <meta property="og:type" content="article" />
    <meta property="og:url" content="${req.protocol}://${req.get('host')}${req.originalUrl}" />${ogImage ? `
    <meta property="og:image" content="${escape(ogImage)}" />` : ''}
    <meta name="twitter:card" content="${ogImage ? 'summary_large_image' : 'summary'}" />`;

        const html = indexHtml.replace('</head>', ogTags + '\n  </head>');
        res.send(html);
      } catch (err) {
        console.error('OG meta error:', err.message);
        res.sendFile(path.join(clientDist, 'index.html'));
      }
    });

    app.get('*', (req, res) => {
      if (!req.path.startsWith('/api')) {
        res.sendFile(path.join(clientDist, 'index.html'));
      }
    });
  }

  app.listen(PORT, () => {
    console.log(`Digital Hub API running on http://localhost:${PORT}`);
    console.log(`Uploads directory: ${uploadsDir}`);
  });
}
startServer();
