import { Hono } from 'hono';
import { serveStatic } from 'hono/bun';
import fs from 'fs';
// @ts-ignore Bun project may not have dotenv types configured
import { config } from 'dotenv';
import { getCache, setCache, isRedisConnected } from './redis-cache';

config();

const app = new Hono();

// Service Principal token cache
let appOnlyToken: string | null = null;
let appOnlyTokenExpiry: number = 0;

// Get app-only token using client credentials flow
async function getAppOnlyToken(): Promise<string | null> {
  try {
    // Check if we have a valid cached token
    if (appOnlyToken && appOnlyTokenExpiry > Date.now()) {
      return appOnlyToken;
    }

    const clientId = process.env.MICROSOFT_CLIENT_ID;
    const clientSecret = process.env.MICROSOFT_CLIENT_SECRET;
    const tenant = process.env.MICROSOFT_TENANT || 'common';

    if (!clientId || !clientSecret) {
      return null; // Service principal not configured
    }

    const tokenUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`;
    
    const params = new URLSearchParams({
      client_id: clientId,
      client_secret: clientSecret,
      scope: 'https://graph.microsoft.com/.default',
      grant_type: 'client_credentials'
    });

    const response = await fetch(tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: params.toString()
    });

    if (!response.ok) {
      const error = await response.text();
      logLine('logs/error.log', `Failed to get app-only token: ${error}`);
      return null;
    }

    const data = await response.json() as any;
    appOnlyToken = data.access_token;
    // Set expiry to 55 minutes (token is valid for 60 minutes)
    appOnlyTokenExpiry = Date.now() + (55 * 60 * 1000);
    
    logLine('logs/server.log', `✓ Got app-only token (expires in 55 minutes)`);
    return appOnlyToken;
  } catch (err) {
    logLine('logs/error.log', `getAppOnlyToken error: ${(err as Error)?.message || String(err)}`);
    return null;
  }
}

// Minimal log helpers
const logLine = (path: string, line: string) => {
  try {
    fs.appendFileSync(path, `${new Date().toISOString()} ${line}\n`);
  } catch (err) {
    console.error('Failed to write log:', err);
  }
};

// Version endpoint sourced from package.json
app.get('/version', (c) => {
  try {
    const pkgRaw = fs.readFileSync('./package.json', 'utf-8');
    const pkg = JSON.parse(pkgRaw);
    return c.json({ version: pkg.version || '0.0.0' });
  } catch (err) {
    logLine('logs/error.log', `version endpoint error: ${(err as Error)?.message || String(err)}`);
    return c.json({ version: '0.0.0' }, 200);
  }
});

// Jobs endpoint with Redis caching
app.get('/api/jobs', async (c) => {
  const page = parseInt(c.req.query('page') || '1');
  const perPage = parseInt(c.req.query('perPage') || '50');
  const sort = c.req.query('sort') || '-Job_Number';
  
  const cacheKey = `jobs:page:${page}:perPage:${perPage}:sort:${sort}`;
  const ttl = 300; // 5 minutes cache
  
  try {
    // Try to get from cache first
    if (isRedisConnected()) {
      const cached = await getCache(cacheKey);
      if (cached) {
        logLine('logs/server.log', `Cache HIT for ${cacheKey}`);
        return c.json(cached);
      }
      logLine('logs/server.log', `Cache MISS for ${cacheKey}`);
    }
    
    // Fetch from PocketBase
    const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=${sort}`;
    const authHeader = c.req.header('Authorization') || '';
    
    const response = await fetch(pbUrl, {
      headers: authHeader ? { Authorization: authHeader } : {}
    });
    
    if (!response.ok) {
      throw new Error(`PocketBase returned ${response.status}`);
    }
    
    const data = await response.json();
    
    // Cache the result
    if (isRedisConnected()) {
      await setCache(cacheKey, data, ttl);
      logLine('logs/server.log', `Cached ${cacheKey} for ${ttl}s`);
    }
    
    return c.json(data);
    
  } catch (err) {
    logLine('logs/error.log', `jobs endpoint error: ${(err as Error)?.message || String(err)}`);
    return c.json({ error: 'Failed to fetch jobs' }, 500);
  }
});

// Fast endpoint: returns all jobs in a single cached request
app.get('/api/jobs-all', async (c) => {
  const cacheKey = 'jobs:all';
  const ttl = 1800; // 30 minutes cache
  try {
    // Try cache first for instant response
    if (isRedisConnected()) {
      const cached = await getCache(cacheKey);
      if (cached) {
        // Return first 100 jobs immediately for instant display, rest loads in background
        const fastBatch = Array.isArray(cached.items) ? cached.items.slice(0, 100) : [];
        const hasMore = cached.items && cached.items.length > 100;
        
        logLine('logs/server.log', `Cache HIT for ${cacheKey} - sending ${fastBatch.length} jobs (${hasMore ? 'partial' : 'complete'})`);
        
        // Refresh cache in background
        setImmediate(async () => {
          try {
            await refreshJobsCache(cacheKey, ttl, c.req.header('Authorization') || '');
          } catch (err) {}
        });
        
        return c.json({ 
          page: 1, 
          perPage: fastBatch.length, 
          totalItems: cached.totalItems || fastBatch.length,
          totalPages: 1,
          items: fastBatch, 
          partial: hasMore 
        });
      } else {
        // No cache: return empty array instantly, trigger background fetch
        setImmediate(async () => {
          try {
            const result = await fetchAllJobsFromPocketBase(c.req.header('Authorization') || '');
            if (isRedisConnected()) {
              await setCache(cacheKey, result, ttl);
              logLine('logs/server.log', `Cached ${cacheKey} with ${result.items.length} jobs for ${ttl}s`);
            }
          } catch (err) {
            logLine('logs/error.log', `jobs-all background fetch error: ${(err as Error)?.message || String(err)}`);
          }
        });
        return c.json({ items: [], partial: true });
      }
    }
    // Redis not connected: fallback to direct fetch (blocking)
    const result = await fetchAllJobsFromPocketBase(c.req.header('Authorization') || '');
    return c.json(result);
  } catch (err) {
    logLine('logs/error.log', `jobs-all endpoint error: ${(err as Error)?.message || String(err)}`);
    return c.json({ error: 'Failed to fetch jobs' }, 500);
  }
});

// Get remaining jobs after initial batch
app.get('/api/jobs-remaining', async (c) => {
  const cacheKey = 'jobs:all';
  try {
    if (isRedisConnected()) {
      const cached = await getCache(cacheKey);
      if (cached && cached.items && cached.items.length > 100) {
        const remaining = cached.items.slice(100);
        return c.json({ 
          items: remaining,
          totalItems: cached.totalItems || cached.items.length
        });
      }
    }
    return c.json({ items: [] });
  } catch (err) {
    logLine('logs/error.log', `jobs-remaining error: ${(err as Error)?.message || String(err)}`);
    return c.json({ error: 'Failed to fetch remaining jobs' }, 500);
  }
});

// Helper function to fetch all jobs from PocketBase
async function fetchAllJobsFromPocketBase(authHeader: string): Promise<any> {
  const allItems: any[] = [];
  let page = 1;
  const perPage = 500;
  
  while (true) {
    const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=-Job_Number`;
    const response = await fetch(pbUrl, {
      headers: authHeader ? { Authorization: authHeader } : {}
    });
    
    if (!response.ok) {
      throw new Error(`PocketBase returned ${response.status}`);
    }
    
    const data = await response.json();
    const items = data.items || [];
    
    if (items.length === 0) break;
    allItems.push(...items);
    
    if (allItems.length >= (data.totalItems || 0)) break;
    page++;
  }
  
  return {
    page: 1,
    perPage: allItems.length,
    totalItems: allItems.length,
    totalPages: 1,
    items: allItems
  };
}

// Helper function to refresh jobs cache in background
async function refreshJobsCache(cacheKey: string, ttl: number, authHeader: string): Promise<void> {
  try {
    const result = await fetchAllJobsFromPocketBase(authHeader);
    await setCache(cacheKey, result, ttl);
    logLine('logs/server.log', `Background refresh: cached ${cacheKey} with ${result.items.length} jobs`);
  } catch (err) {
    logLine('logs/error.log', `Background refresh error: ${(err as Error)?.message || String(err)}`);
  }
}


// --- Microsoft Graph helpers ---
const getGraphToken = (c?: any) => {
  const headerToken = c?.req?.header ? c.req.header('x-graph-token') : '';
  const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
  const token = headerToken || envToken;
  console.log('[getGraphToken] Header token:', headerToken ? headerToken.substring(0, 20) + '...' : 'NONE');
  console.log('[getGraphToken] Env token:', envToken ? 'SET' : 'NOT SET');
  console.log('[getGraphToken] Using:', token ? token.substring(0, 20) + '...' : 'NONE');
  return token;
};

const b64Url = (input: string) =>
  Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');

type DriveItem = {
  id: string;
  name: string;
  webUrl?: string;
  size?: number;
  lastModifiedDateTime?: string;
  file?: { mimeType?: string };
  folder?: { childCount?: number };
  parentReference?: { path?: string; driveId?: string };
};

const fetchJson = async (url: string, token: string) => {
  const res = await fetch(url, {
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: 'application/json',
    },
  });
  if (!res.ok) {
    const text = await res.text();
    const err: any = new Error(`Graph ${res.status}: ${text}`);
    err.status = res.status;
    throw err;
  }
  return res.json();
};

const resolveShareLink = async (link: string, token: string) => {
  // Decode the link in case it comes pre-encoded from the database
  const decodedLink = decodeURIComponent(link);
  console.log('[resolveShareLink] Decoded link:', decodedLink);
  
  // Check if this is a short sharing link (/:f:/ or /:b:/) or a direct document library URL
  if (decodedLink.includes('/:f:/') || decodedLink.includes('/:b:/')) {
    // Short sharing link - use shares API
    const encoded = `u!${b64Url(decodedLink)}`;
    console.log('[resolveShareLink] Using shares API, encoded:', encoded);
    const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
    return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
  } else {
    // Direct document library URL - parse it and use drive API
    // Example: https://czflex.sharepoint.com/sites/Team/Shared Documents/Forms/AllItems.aspx?RootFolder=/sites/Team/Shared Documents/General/...
    const url = new URL(decodedLink);
    const pathMatch = url.hostname.match(/^([^.]+)\.sharepoint\.com$/);
    if (!pathMatch) throw new Error('Invalid SharePoint URL');
    
    const hostname = pathMatch[1]; // e.g., 'czflex'
    const sitePath = url.pathname.split('/Shared')[0]; // e.g., /sites/Team
    const rootFolderParam = url.searchParams.get('RootFolder');
    if (!rootFolderParam) throw new Error('RootFolder parameter missing');
    
    // Extract the folder path relative to the document library
    // RootFolder format: /sites/Team/Shared Documents/General/Operations [Server]/...
    // We need: General/Operations [Server]/...
    const folderPath = rootFolderParam.split('/Shared Documents/')[1];
    if (!folderPath) throw new Error('Could not parse folder path');
    
    console.log('[resolveShareLink] Hostname:', hostname);
    console.log('[resolveShareLink] Site path:', sitePath);
    console.log('[resolveShareLink] Folder path:', folderPath);
    
    // Get the site ID first using hostname:sitePath format
    const siteData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${hostname}.sharepoint.com:${sitePath}`, token);
    const siteId = siteData.id;
    
    // Get the default document library drive
    const driveData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${siteId}/drive`, token);
    const driveId = driveData.id;
    
    // Get the folder item by path
    const itemData = await fetchJson(
      `https://graph.microsoft.com/v1.0/drives/${driveId}/root:/${folderPath}`,
      token
    );
    
    console.log('[resolveShareLink] Resolved to driveId:', driveId, 'itemId:', itemData.id);
    return { driveId, itemId: itemData.id };
  }
};

const listChildrenRecursive = async (
  driveId: string,
  itemId: string,
  depth = 0,
  maxDepth = 5,
  token: string,
): Promise<DriveItem[]> => {
  const out: DriveItem[] = [];
  const data = await fetchJson(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/children`, token);
  for (const child of data.value || []) {
    out.push(child as DriveItem);
    if (child.folder && depth < maxDepth) {
      const nested = await listChildrenRecursive(driveId, child.id, depth + 1, maxDepth, token);
      out.push(...nested);
    }
  }
  return out;
};

const searchWithinFolder = async (driveId: string, itemId: string, query: string, token: string): Promise<DriveItem[]> => {
  const data = await fetchJson(
    `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/search(q='${encodeURIComponent(query)}')`,
    token,
  );
  return (data.value || []) as DriveItem[];
};

const filterByCategory = (items: DriveItem[], category?: string, pdfOnly: boolean = true) => {
  // First filter: show PDF files and Word documents (which will be converted to PDF)
  let filtered = items;
  if (pdfOnly) {
    filtered = items.filter((i) => {
      if (i.folder) return false;
      const name = i.name.toLowerCase();
      const mimeType = i.file?.mimeType?.toLowerCase() || '';
      // Include PDFs, Word documents, and images
      return name.endsWith('.pdf') || mimeType.includes('pdf') ||
             name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word') ||
             name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') || 
             name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') || 
             name.endsWith('.webp') || mimeType.includes('image');
    });
  }
  
  // Second filter: category filtering (if specified)
  if (!category) return filtered;
  const nameHas = (name: string, words: string[]) => words.some((w) => name.includes(w));
  const lc = (s: string) => (s || '').toLowerCase();
  const contractWords = ['contract', 'estimate', 'proposal', 'bid', 'award', 'agreement', 'sow'];
  const planWords = ['plan', 'drawing', 'dwg', 'pdf', 'sheet', 'layout'];
  return filtered.filter((i) => {
    const n = lc(i.name);
    if (category === 'contracts') return nameHas(n, contractWords);
    if (category === 'plans') return nameHas(n, planWords);
    return true;
  });
};

// List/search job files via Graph using a shared folder link
app.get('/api/job-files', async (c) => {
  try {
    console.log('[job-files] Request received');
    const link = c.req.query('link');
    if (!link) return c.json({ error: 'link required' }, 400);
    console.log('[job-files] Link:', link);
    
    const q = c.req.query('q') || '';
    const category = c.req.query('category') || '';
    
    // Check cache first (only for non-search, non-category requests)
    if (!q && !category && isRedisConnected()) {
      const cacheKey = `job-files:${link}`;
      const cached = await getCache(cacheKey);
      if (cached) {
        console.log('[job-files] Cache HIT for', link);
        // Apply filtering if requested
        const pdfOnly = c.req.query('pdfOnly') !== 'false';
        const items = cached.items || [];
        const filtered = pdfOnly 
          ? items.filter((i: any) => {
              if (i.isFolder) return false;
              const name = (i.name || '').toLowerCase();
              const mimeType = (i.mimeType || '').toLowerCase();
              return name.endsWith('.pdf') || mimeType.includes('pdf') ||
                     name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word') ||
                     name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') || 
                     name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') || 
                     name.endsWith('.webp') || mimeType.includes('image');
            })
          : items;
        
        const mapped = filtered.map((i: any) => ({
          id: i.id,
          name: i.name,
          url: i.webUrl,
          driveId: i.driveId,
          size: i.size,
          modified: i.lastModifiedDateTime,
          contentType: i.mimeType,
          isFolder: i.isFolder,
          path: i.parentPath || '',
        }));
        
        return c.json({ items: mapped, total: mapped.length, source: 'cache', driveId: cached.driveId });
      }
      console.log('[job-files] Cache MISS for', link);
    }
    
    // Not in cache or search/category request - fetch from Graph API
    const token = getGraphToken(c);
    if (!token) {
      console.log('[job-files] No token found');
      return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
    }
    console.log('[job-files] Token found:', token.substring(0, 20) + '...');

    const { driveId, itemId } = await resolveShareLink(link, token);

    let items: DriveItem[] = [];
    if (q) {
      items = await searchWithinFolder(driveId, itemId, q, token);
    } else {
      // Walk subfolders up to depth 5
      items = await listChildrenRecursive(driveId, itemId, 0, 5, token);
    }

    // Filter to show only PDFs by default (can be disabled with pdfOnly=false query param)
    const pdfOnly = c.req.query('pdfOnly') !== 'false';
    const filtered = filterByCategory(items, category, pdfOnly);
    console.log(`[job-files] Found ${items.length} total items, ${filtered.length} after filtering`);

    const mapped = filtered.map((i) => ({
      id: i.id,
      name: i.name,
      url: i.webUrl,
      driveId: i.parentReference?.driveId || driveId,
      size: i.size,
      modified: i.lastModifiedDateTime,
      contentType: i.file?.mimeType,
      isFolder: Boolean(i.folder),
      path: i.parentReference?.path || '',
    }));

    return c.json({ items: mapped, total: mapped.length, source: q ? 'search' : 'walk', driveId });
  } catch (err) {
    const message = (err as Error)?.message || String(err);
    const status = (err as any)?.status || 500;
    console.error('[job-files] ERROR:', message);
    logLine('logs/error.log', `/api/job-files error: ${message}`);
    return c.json({ error: message }, status);
  }
});

// Stream file content via Graph to avoid CSP issues for inline preview
app.get('/api/job-file-content', async (c) => {
  try {
    const token = getGraphToken(c);
    if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
    const driveId = c.req.query('driveId');
    const itemId = c.req.query('itemId');
    if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);

    const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!res.ok) {
      const text = await res.text();
      logLine('logs/error.log', `/api/job-file-content failed: ${res.status} - ${text}`);
      return c.json({ error: `Graph ${res.status}: ${text}` }, 502);
    }
    const headers: Record<string, string> = {};
    const ct = res.headers.get('content-type');
    const cd = res.headers.get('content-disposition');
    if (ct) headers['Content-Type'] = ct;
    if (cd) headers['Content-Disposition'] = cd;
    return new Response(res.body, { status: 200, headers });
  } catch (err) {
    const message = (err as Error)?.message || String(err);
    const status = (err as any)?.status || 500;
    logLine('logs/error.log', `/api/job-file-content error: ${message}`);
    return c.json({ error: message }, status);
  }
});

// Convert Word documents to PDF for display
app.get('/api/job-file-pdf', async (c) => {
  try {
    const token = getGraphToken(c);
    if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
    const driveId = c.req.query('driveId');
    const itemId = c.req.query('itemId');
    if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);

    // Check cache first
    const cacheKey = `job-file-pdf:${driveId}:${itemId}`;
    if (isRedisConnected()) {
      const cached = await getCache(cacheKey);
      if (cached && cached.data) {
        logLine('logs/server.log', `PDF cache HIT: ${driveId}:${itemId}`);
        const buffer = Buffer.from(cached.data, 'base64');
        const headers: Record<string, string> = {
          'Content-Type': 'application/pdf',
        };
        if (cached.filename) {
          headers['Content-Disposition'] = `inline; filename="${cached.filename}"`;
        }
        return new Response(buffer, { status: 200, headers });
      } else {
        logLine('logs/server.log', `PDF cache MISS: ${driveId}:${itemId}`);
      }
    }

    // Use Graph API to convert to PDF format
    const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content?format=pdf`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!res.ok) {
      const text = await res.text();
      logLine('logs/error.log', `/api/job-file-pdf conversion failed: ${res.status} - ${text}`);
      return c.json({ error: `PDF conversion failed: ${res.status}` }, 502);
    }
    
    const pdfBuffer = Buffer.from(await res.arrayBuffer());
    const headers: Record<string, string> = {
      'Content-Type': 'application/pdf',
    };
    const cd = res.headers.get('content-disposition');
    let filename = 'document.pdf';
    if (cd) {
      // Replace .docx/.doc extension with .pdf in filename
      headers['Content-Disposition'] = cd.replace(/\.(docx?)/gi, '.pdf');
      const match = cd.match(/filename="?([^"]+)"?/);
      if (match) filename = match[1].replace(/\.(docx?)/gi, '.pdf');
    }
    
    // Cache the converted PDF for 30 minutes
    if (isRedisConnected()) {
      await setCache(cacheKey, {
        data: pdfBuffer.toString('base64'),
        filename
      }, 1800);
    }
    
    logLine('logs/server.log', `Successfully converted document to PDF: driveId=${driveId}, itemId=${itemId}`);
    return new Response(pdfBuffer, { status: 200, headers });
  } catch (err) {
    const message = (err as Error)?.message || String(err);
    const status = (err as any)?.status || 500;
    logLine('logs/error.log', `/api/job-file-pdf error: ${message}`);
    return c.json({ error: message }, status);
  }
});

// Mutation logging endpoint
app.post('/log-change', async (c) => {
  try {
    const body = await c.req.json();
    const { action = 'unknown', user = 'unknown', target = '', detail = '' } = body || {};
    const payload = JSON.stringify({ action, user, target, detail });
    logLine('logs/changes.log', payload);
    return c.json({ status: 'ok' });
  } catch (err) {
    logLine('logs/error.log', `log-change error: ${(err as Error)?.message || String(err)}`);
    return c.text('Bad Request', 400);
  }
});

// Serve static files from the frontend directory (must come after API routes)
// Add cache headers for static assets
app.use('/assets/*', async (c, next) => {
  await next();
  // Cache assets for 1 year
  c.header('Cache-Control', 'public, max-age=31536000, immutable');
});
app.use('/*', serveStatic({ root: './frontend/public' }));

// Error handler
app.onError((err, c) => {
  logLine('logs/error.log', `Unhandled error: ${err?.message || err}`);
  return c.text('Internal Server Error', 500);
});

// Default to 3005 for test instance; can be overridden by Environment=PORT
const PORT = Number(process.env.PORT || 3005);

logLine('logs/server.log', `Starting frontend server (Redis version) on port ${PORT}`);

// Warm up cache on startup - wait for Redis to be ready
(async () => {
  // Wait for Redis to be ready (max 10 seconds)
  let retries = 0;
  while (!isRedisConnected() && retries < 20) {
    await new Promise(resolve => setTimeout(resolve, 500));
    retries++;
  }
  
  if (!isRedisConnected()) {
    logLine('logs/error.log', 'Redis not connected after 10 seconds, skipping cache warmup');
    return;
  }
  
  try {
    logLine('logs/server.log', 'Warming up jobs cache...');
    const result = await fetchAllJobsFromPocketBase('');
    await setCache('jobs:all', result, 1800);
    logLine('logs/server.log', `✓ Cache warmed with ${result.items.length} total jobs`);
    
    // Try to start file caching if service principal is configured
    const token = await getAppOnlyToken();
    if (token) {
      logLine('logs/server.log', 'Service principal authenticated, starting background file caching...');
      // Cache files for ALL jobs with folder links
      startFileCaching(result.items, token);
    } else {
      logLine('logs/server.log', 'Service principal not configured, file caching will occur on-demand');
    }
  } catch (err) {
    logLine('logs/error.log', `Cache warmup failed: ${(err as Error)?.message || String(err)}`);
  }
})();

// Background file caching state
let fileCachingActive = false;
let fileCachingIndex = 0;
let fileCachingJobs: any[] = [];
let fileCachingToken: string = '';
let fileCachingTimer: NodeJS.Timeout | null = null;

async function startFileCaching(jobs: any[], token: string) {
  if (fileCachingActive) return;
  
  fileCachingJobs = jobs;
  if (fileCachingJobs.length === 0) {
    logLine('logs/server.log', 'No jobs to cache');
    return;
  }
  
  fileCachingActive = true;
  fileCachingIndex = 0;
  fileCachingToken = token;
  
  logLine('logs/server.log', `Starting background file caching for ${fileCachingJobs.length} jobs...`);
  cacheNextFileBatch();
}

async function cacheNextFileBatch() {
  if (!fileCachingActive || !isRedisConnected()) return;
  
  const batchSize = 10; // Cache 10 jobs at a time for faster initial coverage
  const maxJobs = fileCachingJobs.length;
  
  if (fileCachingIndex >= maxJobs) {
    // Completed initial full cache of all jobs
    console.log(`✓ Completed caching all ${maxJobs} jobs. Scheduling refresh in 10 minutes...`);
    logLine('logs/server.log', `✓ Completed caching all ${maxJobs} jobs. Scheduling refresh in 10 minutes...`);
    fileCachingIndex = 0;
    
    // After completing all jobs, wait 10 minutes before refreshing
    fileCachingTimer = setTimeout(async () => {
      try {
        // Refresh the jobs cache first
        await refreshJobsCache('jobs:all', 1800, '');
        
        // Get updated job list from cache
        const cached = await getCache('jobs:all');
        if (cached && cached.items) {
          fileCachingJobs = cached.items;
          console.log(`Starting refresh cycle: ${fileCachingJobs.length} jobs to update`);
          logLine('logs/server.log', `Starting refresh cycle: ${fileCachingJobs.length} jobs to update`);
        }
        
        // Refresh token if needed
        const newToken = await getAppOnlyToken();
        if (newToken) {
          fileCachingToken = newToken;
          cacheNextFileBatch();
        } else {
          console.error('Failed to refresh app-only token, stopping file caching');
          logLine('logs/error.log', 'Failed to refresh app-only token, stopping file caching');
          fileCachingActive = false;
        }
      } catch (err) {
        console.error(`File cache cycle restart error: ${(err as Error)?.message}`);
        logLine('logs/error.log', `File cache cycle restart error: ${(err as Error)?.message}`);
      }
    }, 10 * 60 * 1000);
    return;
  }
  
  // Get next batch of jobs to cache
  const batch = fileCachingJobs.slice(fileCachingIndex, fileCachingIndex + batchSize);
  const jobNumbers = batch.map(j => j.Job_Number).join(', ');
  const batchMsg = `[Cache Batch] Processing jobs: ${jobNumbers} (${fileCachingIndex}/${maxJobs})`;
  console.log(batchMsg);
  logLine('logs/server.log', batchMsg);
  
  for (const job of batch) {
    try {
      await cacheJobFiles(job, fileCachingToken);
    } catch (err) {
      // Silently continue on errors
      logLine('logs/error.log', `File cache error for job ${job.Job_Number}: ${(err as Error)?.message}`);
    }
  }
  
  fileCachingIndex += batchSize;
  
  // Continue with next batch after a short delay (1 second between batches for faster throughput)
  fileCachingTimer = setTimeout(() => cacheNextFileBatch(), 1000);
}

async function cacheJobFiles(job: any, token: string) {
  const link = job.Job_Folder_Link;
  if (!link) {
    // Job has no folder link, just mark it as cached
    const cacheKey = `job-files:${job.Job_Number}`;
    await setCache(cacheKey, { items: [] }, 1800);
    console.log(`✓ No folder for job ${job.Job_Number}`);
    logLine('logs/server.log', `✓ No folder for job ${job.Job_Number}`);
    return;
  }
  
  const cacheKey = `job-files:${link}`;
  const ttl = 1800; // 30 minutes
  
  try {
    const { driveId, itemId } = await resolveShareLink(link, token);
    const items = await listChildrenRecursive(driveId, itemId, 0, 5, token);
    
    const data = {
      driveId,
      items: items.map(item => ({
        id: item.id,
        name: item.name,
        webUrl: item.webUrl,
        size: item.size,
        lastModifiedDateTime: item.lastModifiedDateTime,
        mimeType: item.file?.mimeType,
        isFolder: !!item.folder,
        parentPath: item.parentReference?.path,
        driveId: item.parentReference?.driveId || driveId
      })),
      cachedAt: Date.now()
    };
    
    await setCache(cacheKey, data, ttl);
    
    // Pre-cache PDF conversions for Word/Excel documents AND native PDF files
    const officeFiles = items.filter(item => {
      const name = (item.name || '').toLowerCase();
      return name.endsWith('.docx') || name.endsWith('.doc') || name.endsWith('.xlsx') || name.endsWith('.xls');
    });
    
    const pdfFiles = items.filter(item => {
      const name = (item.name || '').toLowerCase();
      return name.endsWith('.pdf');
    });
    
    let pdfsCached = 0;
    
    // Cache converted Office documents
    for (const file of officeFiles) {
      try {
        const fileDriveId = file.parentReference?.driveId || driveId;
        const pdfCacheKey = `job-file-pdf:${fileDriveId}:${file.id}`;
        
        // Check if already cached
        const existing = await getCache(pdfCacheKey);
        if (existing) {
          pdfsCached++;
          continue;
        }
        
        // Convert and cache
        const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${fileDriveId}/items/${file.id}/content?format=pdf`, {
          headers: { Authorization: `Bearer ${token}` },
        });
        
        if (res.ok) {
          const pdfBuffer = Buffer.from(await res.arrayBuffer());
          const cd = res.headers.get('content-disposition');
          let filename = file.name.replace(/\.(docx?|xlsx?)$/gi, '.pdf');
          if (cd) {
            const match = cd.match(/filename="?([^"]+)"?/);
            if (match) filename = match[1].replace(/\.(docx?|xlsx?)$/gi, '.pdf');
          }
          
          await setCache(pdfCacheKey, {
            data: pdfBuffer.toString('base64'),
            filename
          }, ttl);
          pdfsCached++;
        }
      } catch (err) {
        // Silently skip PDF caching errors
      }
    }
    
    // Cache native PDF files
    for (const file of pdfFiles) {
      try {
        const fileDriveId = file.parentReference?.driveId || driveId;
        const pdfCacheKey = `job-file-pdf:${fileDriveId}:${file.id}`;
        
        // Check if already cached
        const existing = await getCache(pdfCacheKey);
        if (existing) {
          pdfsCached++;
          continue;
        }
        
        // Fetch and cache native PDF
        const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${fileDriveId}/items/${file.id}/content`, {
          headers: { Authorization: `Bearer ${token}` },
        });
        
        if (res.ok) {
          const pdfBuffer = Buffer.from(await res.arrayBuffer());
          await setCache(pdfCacheKey, {
            data: pdfBuffer.toString('base64'),
            filename: file.name
          }, ttl);
          pdfsCached++;
        }
      } catch (err) {
        // Silently skip PDF caching errors
      }
    }
    
    const totalPDFs = officeFiles.length + pdfFiles.length;
    const msg = `✓ Cached ${items.length} files${totalPDFs > 0 ? ` + ${pdfsCached}/${totalPDFs} PDFs` : ''} for job ${job.Job_Number}`;
    console.log(msg);
    logLine('logs/server.log', msg);
  } catch (err: any) {
    // Only log if not a token expiration issue
    if (!err.message?.includes('401') && !err.message?.includes('token')) {
      logLine('logs/error.log', `File cache skip for job ${job.Job_Number}: ${err.message}`);
    }
  }
}

// Graceful shutdown logging
const shutdown = (signal: string) => {
  logLine('logs/server.log', `Shutting down due to ${signal}`);
  if (fileCachingTimer) clearTimeout(fileCachingTimer);
  process.exit(0);
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));

export default {
  port: PORT,
  fetch: app.fetch,
};
