import { Hono } from 'hono';
import { serveStatic } from 'hono/bun';
import { cors } from 'hono/cors';
import PocketBase from 'pocketbase';
import { ConfidentialClientApplication } from '@azure/msal-node';
import { readFileSync } from 'fs';
import { join } from 'path';

// Load env from Estimator/.env
const envPath = join(import.meta.dir, 'Estimator', '.env');
try {
  const envContent = readFileSync(envPath, 'utf-8');
  envContent.split('\n').forEach(line => {
    const trimmed = line.trim();
    if (trimmed && !trimmed.startsWith('#')) {
      const [key, ...valueParts] = trimmed.split('=');
      if (key && valueParts.length > 0) {
        process.env[key.trim()] = valueParts.join('=').trim();
      }
    }
  });
  console.log('✓ Loaded env from Estimator/.env');
} catch (e) {
  console.warn('⚠ Could not load Estimator/.env:', (e as Error).message);
}

const app = new Hono();

// Enable CORS
app.use('/*', cors({ 
  origin: '*',
  credentials: true 
}));

// PocketBase client (fixed URL per request)
const pb = new PocketBase('https://pocketbase.ccllc.pro');

// Use user's PocketBase token
function setUserPocketBaseAuth(token: string) {
  pb.authStore.save(token, null);
}

// MSAL config for Graph token (client credentials)
const msalConfig = {
  auth: {
    clientId: process.env.CLIENT_ID || '',
    authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
    clientSecret: process.env.CLIENT_SECRET || ''
  }
};
const cca = new ConfidentialClientApplication(msalConfig);

async function getGraphToken(): Promise<string> {
  const result = await cca.acquireTokenByClientCredential({
    scopes: ['https://graph.microsoft.com/.default']
  });
  if (!result || !result.accessToken) {
    throw new Error('Failed to acquire Graph token');
  }
  return result.accessToken;
}

// Health check
app.get('/health', (c) => {
  return c.json({ ok: true, pbDB: process.env.PB_DB });
});

// Submit endpoint
app.post('/api/submit', async (c) => {
  try {
    const body = await c.req.json();
    const pbToken = body.pbToken;
    
    if (!pbToken) {
      return c.json({ success: false, message: 'Missing pbToken' }, 401);
    }
    
    // Validate user token
    setUserPocketBaseAuth(pbToken);
    try {
      await pb.collection(process.env.PB_AUTH_COLLECTION || 'Users').authRefresh();
    } catch (e) {
      console.error('auth_validation_failed', { message: (e as Error)?.message });
      return c.json({ success: false, message: 'Invalid token' }, 401);
    }
    
    // Store in PocketBase collection
    const record = await pb.collection(process.env.PB_COLLECTION || 'Job_Info_TestEnv').create({
      ...body,
      submittedBy: pb.authStore.model?.email || 'unknown',
      submittedAt: new Date().toISOString()
    });
    
    console.log('submission_success', { recordId: record.id });
    return c.json({ success: true, message: 'Feedback submitted successfully', recordId: record.id });
  } catch (err) {
    console.error('submit_error', { message: (err as Error)?.message });
    return c.json({ success: false, message: 'Internal error' }, 500);
  }
});

// Serve static index.html
app.get('/', async (c) => {
  const html = await Bun.file('index.html').text();
  return c.html(html);
});

// Admin login endpoint
app.post('/api/admin/login', async (c) => {
  try {
    const { email, password } = await c.req.json();
    if (!email || !password) {
      return c.json({ message: 'Missing email or password' }, 400);
    }
    
    // Authenticate as PocketBase superuser via `_superusers` collection
    const authData = await pb.collection('_superusers').authWithPassword(email, password);
    if (!authData || !authData.token) {
      return c.json({ message: 'Invalid credentials' }, 401);
    }
    return c.json({ ok: true, token: authData.token, email: authData.record?.email, role: 'superuser' });
  } catch (err) {
    console.error('admin_login_error', { message: (err as Error)?.message });
    return c.json({ message: 'Login failed' }, 401);
  }
});

// Get collection schema (requires admin token)
app.get('/api/schema', async (c) => {
  try {
    const authHeader = c.req.header('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return c.json({ message: 'Missing authorization token' }, 401);
    }
    
    const token = authHeader.slice(7);
    setUserPocketBaseAuth(token);
    
    // Verify superuser token; `_superusers` can fetch schema
    try {
      await pb.collection('_superusers').authRefresh();
    } catch (e) {
      return c.json({ message: 'Invalid or non-superuser token' }, 401);
    }
    
    const url = new URL(c.req.url);
    const requestedName = url.searchParams.get('name');
    const collectionName = requestedName || process.env.PB_COLLECTION || 'Job_Info_TestEnv';
    const pbDb = process.env.PB_DB || 'https://pocketbase.ccllc.pro';
    
    // Try SDK first
    let schema: any[] = [];
    let meta: Record<string, any> = {};
    try {
      const collection = await pb.collections.getOne(collectionName);
      schema = Array.isArray(collection?.schema) ? collection.schema : [];
      meta = {
        name: collection?.name,
        type: collection?.type,
        listRule: collection?.listRule,
        viewRule: collection?.viewRule,
        createRule: collection?.createRule,
        updateRule: collection?.updateRule,
        deleteRule: collection?.deleteRule,
      };
    } catch (e) {
      // ignore and try REST fallback
    }
    
    if (!schema.length) {
      // Fallback to REST API for robustness
      const resp = await fetch(`${pbDb}/api/collections/${encodeURIComponent(collectionName)}`, {
        headers: { Authorization: `Bearer ${token}` }
      });
      if (resp.ok) {
        const data = await resp.json();
        schema = Array.isArray(data?.schema) ? data.schema : [];
        meta = {
          name: data?.name,
          type: data?.type,
          listRule: data?.listRule,
          viewRule: data?.viewRule,
          createRule: data?.createRule,
          updateRule: data?.updateRule,
          deleteRule: data?.deleteRule,
        };
      }
    }
    
    if (!schema.length) {
      // As a last resort, try to derive field names and generate schema from sample records
      try {
        const perPage = 25;
        const list = await pb.collection(collectionName).getList(1, perPage);
        const items = list?.items || [];
        if (items.length) {
          const systemKeys = new Set(['id','created','updated','collectionId','collectionName','expand']);
          const keyStats: Record<string, { count: number; types: Set<string> }> = {};
          for (const it of items) {
            for (const k of Object.keys(it)) {
              if (systemKeys.has(k)) continue;
              const v = (it as any)[k];
              const t = Array.isArray(v) ? 'array' : (v === null ? 'null' : typeof v);
              if (!keyStats[k]) keyStats[k] = { count: 0, types: new Set() };
              if (v !== undefined && v !== null) keyStats[k].count += 1;
              keyStats[k].types.add(t);
            }
          }
          const generatedSchema = Object.entries(keyStats).map(([name, stat]) => ({
            name,
            type: Array.from(stat.types).join('|'),
            required: stat.count === items.length
          }));
          return c.json({ ok: true, schema: [], meta, generatedSchema, message: 'Generated schema from sample records.' });
        }
      } catch {}
      return c.json({ ok: true, schema: [], meta, message: 'No fields found. Check collection name or permissions.' });
    }
    
    return c.json({ ok: true, schema, meta });
  } catch (err) {
    console.error('schema_fetch_error', { message: (err as Error)?.message });
    return c.json({ message: 'Internal error' }, 500);
  }
});

// List all collections (requires superuser token)
app.get('/api/collections', async (c) => {
  try {
    const authHeader = c.req.header('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return c.json({ message: 'Missing authorization token' }, 401);
    }
    const token = authHeader.slice(7);
    setUserPocketBaseAuth(token);

    try {
      await pb.collection('_superusers').authRefresh();
    } catch (e) {
      return c.json({ message: 'Invalid or non-superuser token' }, 401);
    }

    const list = await pb.collections.getFullList();
    const items = list.map((c) => ({
      id: (c as any).id,
      name: (c as any).name,
      type: (c as any).type,
      schemaCount: Array.isArray((c as any).schema) ? (c as any).schema.length : 0,
    }));
    return c.json({ ok: true, items });
  } catch (err) {
    console.error('collections_list_error', { message: (err as Error)?.message });
    return c.json({ message: 'Internal error' }, 500);
  }
});

// Get a single field definition and sample values (requires superuser token)
app.get('/api/field', async (c) => {
  try {
    const authHeader = c.req.header('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return c.json({ message: 'Missing authorization token' }, 401);
    }
    const token = authHeader.slice(7);
    setUserPocketBaseAuth(token);

    // Verify superuser
    try {
      await pb.collection('_superusers').authRefresh();
    } catch (e) {
      return c.json({ message: 'Invalid or non-superuser token' }, 401);
    }

    const url = new URL(c.req.url);
    const collectionName = url.searchParams.get('name') || process.env.PB_COLLECTION || 'Job_Info_TestEnv';
    const fieldNameParam = url.searchParams.get('field') || 'job_number';
    const lc = (s: string) => s.toLowerCase();
    const candidates = Array.from(new Set([
      fieldNameParam,
      lc(fieldNameParam),
      fieldNameParam.toUpperCase(),
      fieldNameParam.replace(/[\s-]/g, '_'),
      lc(fieldNameParam.replace(/[\s-]/g, '_')),
      // naive camelCase variants
      fieldNameParam.replace(/[_\s-]([a-z])/g, (_, ch) => ch.toUpperCase()),
      lc(fieldNameParam.replace(/[_\s-]([a-z])/g, (_, ch) => ch.toUpperCase()))
    ]));

    let definition: any = null;
    let derived = false;
    let values: any[] = [];
    let totalItems = 0;
    let resolvedField: string | null = null;

    // Try getting field definition via SDK
    try {
      const collection = await pb.collections.getOne(collectionName);
      if (Array.isArray(collection?.schema)) {
        definition = collection.schema.find((f: any) => candidates.some((cand) => lc(f?.name || '') === lc(cand))) || null;
        if (definition) resolvedField = definition.name;
      }
    } catch {}

    // If no explicit schema, derive from a sample record
    if (!definition) {
      try {
        const list = await pb.collection(collectionName).getList(1, 1);
        const sample = list?.items?.[0];
        if (sample) {
          const sampleKeys = Object.keys(sample);
          const matchKey = sampleKeys.find(k => candidates.some(c => lc(k) === lc(c)));
          if (matchKey) {
            derived = true;
            resolvedField = matchKey;
          }
        }
      } catch {}
    }

    // Collect up to 10 values for preview
    try {
      const list = await pb.collection(collectionName).getList(1, 1);
      totalItems = list?.totalItems ?? 0;
    } catch {}

    try {
      const list = await pb.collection(collectionName).getList(1, 20);
      const items = list?.items || [];
      values = items.map((it: any) => {
        if (resolvedField && Object.prototype.hasOwnProperty.call(it, resolvedField)) return it[resolvedField];
        // last attempt: case-insensitive per record
        const itKeys = Object.keys(it);
        const k = itKeys.find(k => candidates.some(c => lc(k) === lc(c)));
        return k ? it[k] : undefined;
      }).filter((v: any) => v !== undefined && v !== null);
    } catch {}

    const nonNullCount = values.length;
    return c.json({ ok: true, collection: collectionName, field: fieldNameParam, resolvedField, definition, derived, values, count: nonNullCount, totalItems });
  } catch (err) {
    console.error('field_fetch_error', { message: (err as Error)?.message });
    return c.json({ message: 'Internal error' }, 500);
  }
});

// Get sample records for a collection (requires superuser token)
app.get('/api/records', async (c) => {
  try {
    const authHeader = c.req.header('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return c.json({ message: 'Missing authorization token' }, 401);
    }
    const token = authHeader.slice(7);
    setUserPocketBaseAuth(token);

    try {
      await pb.collection('_superusers').authRefresh();
    } catch (e) {
      return c.json({ message: 'Invalid or non-superuser token' }, 401);
    }

    const url = new URL(c.req.url);
    const collectionName = url.searchParams.get('name') || process.env.PB_COLLECTION || 'Job_Info_TestEnv';
    const perPage = Math.max(1, Math.min(100, Number(url.searchParams.get('perPage') || 10)));

    const firstPage = await pb.collection(collectionName).getList(1, perPage);
    const items = firstPage?.items || [];
    return c.json({ ok: true, collection: collectionName, totalItems: firstPage?.totalItems ?? 0, items });
  } catch (err) {
    console.error('records_fetch_error', { message: (err as Error)?.message });
    return c.json({ message: 'Internal error' }, 500);
  }
});

// Serve admin.html
app.get('/admin', async (c) => {
  const html = await Bun.file(new URL('./admin.html', import.meta.url)).text();
  return c.html(html);
});

// Export collection schema and headers (requires superuser token)
app.get('/api/export/schema', async (c) => {
  try {
    const authHeader = c.req.header('Authorization');
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return c.json({ message: 'Missing authorization token' }, 401);
    }
    const token = authHeader.slice(7);
    setUserPocketBaseAuth(token);

    try {
      await pb.collection('_superusers').authRefresh();
    } catch (e) {
      return c.json({ message: 'Invalid or non-superuser token' }, 401);
    }

    const url = new URL(c.req.url);
    const collectionName = url.searchParams.get('name') || process.env.PB_COLLECTION || 'Job_Info_TestEnv';

    let schema: any[] = [];
    let meta: Record<string, any> = {};
    let generatedSchema: any[] = [];

    // Fetch explicit schema via SDK
    try {
      const collection = await pb.collections.getOne(collectionName);
      schema = Array.isArray(collection?.schema) ? collection.schema : [];
      meta = {
        id: (collection as any)?.id,
        name: (collection as any)?.name,
        type: (collection as any)?.type,
        listRule: (collection as any)?.listRule,
        viewRule: (collection as any)?.viewRule,
        createRule: (collection as any)?.createRule,
        updateRule: (collection as any)?.updateRule,
        deleteRule: (collection as any)?.deleteRule,
      };
    } catch {}

    // If no explicit schema, generate from records
    if (!schema.length) {
      try {
        const perPage = 100;
        const list = await pb.collection(collectionName).getList(1, perPage);
        const items = list?.items || [];
        if (items.length) {
          const systemKeys = new Set(['id','created','updated','collectionId','collectionName','expand']);
          const keyStats: Record<string, { count: number; types: Set<string> }> = {};
          for (const it of items) {
            for (const k of Object.keys(it)) {
              if (systemKeys.has(k)) continue;
              const v = (it as any)[k];
              const t = Array.isArray(v) ? 'array' : (v === null ? 'null' : typeof v);
              if (!keyStats[k]) keyStats[k] = { count: 0, types: new Set() };
              if (v !== undefined && v !== null) keyStats[k].count += 1;
              keyStats[k].types.add(t);
            }
          }
          generatedSchema = Object.entries(keyStats).map(([name, stat]) => ({
            name,
            type: Array.from(stat.types).join('|'),
            required: stat.count === items.length,
            occurrences: stat.count,
            sampleSize: items.length
          }));
        }
      } catch {}
    }

    // Collect all unique headers (field names)
    const headers = new Set<string>();
    try {
      const perPage = 100;
      const list = await pb.collection(collectionName).getList(1, perPage);
      const items = list?.items || [];
      for (const it of items) {
        for (const k of Object.keys(it)) {
          if (!['id','created','updated','collectionId','collectionName','expand'].includes(k)) {
            headers.add(k);
          }
        }
      }
    } catch {}

    const exportData = {
      exportedAt: new Date().toISOString(),
      collection: collectionName,
      meta,
      schema: schema.length ? schema : generatedSchema,
      schemaType: schema.length ? 'explicit' : 'generated',
      headers: Array.from(headers).sort(),
    };

    c.header('Content-Type', 'application/json');
    c.header('Content-Disposition', `attachment; filename="${collectionName}-schema-${new Date().toISOString().split('T')[0]}.json"`);
    return c.json(exportData);
  } catch (err) {
    console.error('export_schema_error', { message: (err as Error)?.message });
    return c.json({ message: 'Internal error' }, 500);
  }
});

const PORT = Number(process.env.PORT || 7500);

console.log(`Idea & Feedback Form server running at http://localhost:${PORT}`);

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