
import './telemetry';
import { otelLogger, SeverityNumber, meter } from './telemetry';
import Fastify, { FastifyInstance, FastifyError } from 'fastify';

declare module 'fastify' {
  interface FastifyRequest {
    errorHandled?: boolean;
    meta?: Record<string, string>;
  }
}

function classifyError(error: FastifyError): string {
  if (error.code === 'FST_ERR_VALIDATION') return 'validation_error';
  const msg = error.message.toLowerCase();
  if (msg.includes('geocode') || msg.includes('nominatim')) return 'upstream_error:nominatim';
  if (msg.includes('sponsors') || msg.includes('github')) return 'upstream_error:github';
  if (msg.includes('zammad') || msg.includes('ticket')) return 'upstream_error:zammad';
  if (msg.includes('turnstile') || msg.includes('siteverify')) return 'upstream_error:turnstile';
  return 'internal_error';
}

function classifyByStatus(statusCode: number): string {
  if (statusCode === 404) return 'not_found';
  if (statusCode === 400) return 'client_error';
  if (statusCode === 401 || statusCode === 403) return 'auth_error';
  if (statusCode >= 400 && statusCode < 500) return 'client_error';
  return 'internal_error';
}
import cors from '@fastify/cors';
import { NominatimClient, NominatimResultSchema } from './services/NominatimClient';
import { classifyGeoQuery } from './services/GeoQueryClassifier';
import { GithubClient, SponsorsResponseSchema } from './services/GithubClient';
import { TurnstileClient } from './services/TurnstileClient';
import { ZammadClient, ContactMessageBodySchema, ContactMessageBody } from './services/ZammadClient';

const start = async () => {
  const server: FastifyInstance = Fastify({
    logger: {
      level: 'error',
      transport: {
        target: 'pino-pretty',
        options: {
          colorize: true,
          translateTime: 'HH:MM:ss Z',
          ignore: 'pid,hostname',
        },
      },
    },
    trustProxy: true,
  });

  // Global error handler
  server.setErrorHandler((error: FastifyError, request, reply) => {
    const errorType = classifyError(error);
    const statusCode = error.statusCode ?? 500;

    otelLogger.emit({
      severityNumber: SeverityNumber.ERROR,
      severityText: 'ERROR',
      body: error.message,
      attributes: {
        'error.type': errorType,
        'http.route': (request.routeOptions as { url?: string })?.url ?? '',
        'http.request.method': request.method,
        'http.response.status_code': statusCode,
        'exception.message': error.message,
        'exception.stacktrace': error.stack ?? '',
      },
    });

    request.errorHandled = true;

    if (statusCode !== 404) {
      server.log.error({
        url: request.url,
        method: request.method,
        error: error.message,
        stack: error.stack,
      }, 'Request error');
    }

    reply.status(statusCode).send({ error: 'Internal Server Error' });
  });

  // Coors Banquet Config
  await server.register(cors, {
    origin: (origin, cb) => {
      const allowedOrigins = [
        'http://localhost:5173', // DeFlock Legacy
        'http://localhost:3000', // FlockHopper
        'https://deflock.org',
        'https://www.deflock.org',
        'https://maps.deflock.org'
      ];
      
      if (!origin || allowedOrigins.includes(origin) || /^https:\/\/.*\.deflock\.pages\.dev$/.test(origin)) {
        cb(null, true);
      } else {
        cb(null, false);
      }
    },
    methods: ['GET', 'HEAD', 'POST'],
  });

  server.addHook('onResponse', (request, reply, done) => {
    const statusCode = reply.statusCode;
    const route = (request.routeOptions as { url?: string })?.url ?? request.url.split('?')[0];
    requestCounter.add(1, {
      'http.route': route,
      'http.request.method': request.method,
      'http.response.status_code': statusCode,
      ...request.meta,
    });
    if (!request.errorHandled && statusCode >= 400 && statusCode !== 404) {
      otelLogger.emit({
        severityNumber: statusCode >= 500 ? SeverityNumber.ERROR : SeverityNumber.WARN,
        severityText: statusCode >= 500 ? 'ERROR' : 'WARN',
        body: `HTTP ${statusCode} ${request.method} ${route}`,
        attributes: {
          'error.type': classifyByStatus(statusCode),
          'http.route': route,
          'http.request.method': request.method,
          'http.response.status_code': statusCode,
          ...request.meta,
        },
      });
    }
    done();
  });

  const requestCounter = meter.createCounter('http.server.requests.total', {
    description: 'Total number of HTTP requests, by route, method, and status code',
  });

  const nominatim = new NominatimClient();
  const githubClient = new GithubClient();
  const turnstileClient = new TurnstileClient();
  const zammadClient = new ZammadClient();

  const shutdown = async () => {
    server.log.info("Shutting down");
    await server.close();
    process.exit(0);
  };

  process.on('SIGINT', shutdown);
  process.on('SIGTERM', shutdown);

  server.get('/geocode', {
    schema: {
      querystring: {
        type: 'object',
        properties: {
          query: { type: 'string' },
        },
        required: ['query'],
      },
      response: {
        200: NominatimResultSchema,
        404: { type: 'object', properties: { error: { type: 'string' } } },
        500: { type: 'object', properties: { error: { type: 'string' } } },
      },
    },
  }, async (request, reply) => {
    const { query } = request.query as { query: string };
    reply.header('Cache-Control', 'public, max-age=86400, s-maxage=86400');
    const result = await nominatim.geocodeSingleResult(query);
    if (!result) {
      return reply.status(404).send({ error: 'No results found' });
    }
    return result;
  });

  server.get('/geocode/multi', {
    schema: {
      querystring: {
        type: 'object',
        properties: {
          query: { type: 'string' },
          source: { type: 'string' },
        },
        required: ['query'],
      },
      response: {
        200: {
          type: 'array',
          items: NominatimResultSchema,
        },
        500: { type: 'object', properties: { error: { type: 'string' } } },
      },
    },
  }, async (request, reply) => {
    const { query, source } = request.query as { query: string, source?: string };
    const geoType = classifyGeoQuery(query);
    request.meta = {
      'geocode.source': geoType === 'zip_code' ? 'local_zip' : 'nominatim',
      'geocode.initiator': source ?? '',
      'geocode.query_type': geoType,
    };
    reply.header('Cache-Control', 'public, max-age=86400, s-maxage=86400');
    const result = await nominatim.geocodePhrase(query, false);
    return result;
  });

  server.get('/sponsors/github', {
    schema: {
      response: {
        200: SponsorsResponseSchema,
        500: { type: 'object', properties: { error: { type: 'string' } } },
      },
    },
  }, async (request, reply) => {
    reply.header('Cache-Control', 'public, max-age=60, s-maxage=600');
    const result = await githubClient.getSponsors();
    return result;
  });

  server.post('/contact/message', {
    schema: {
      body: ContactMessageBodySchema,
      response: {
        201: { type: 'object', properties: {} },
        400: { type: 'object', properties: { error: { type: 'string' } } },
        500: { type: 'object', properties: { error: { type: 'string' } } },
      },
    },
  }, async (request, reply) => {
    const { name, email, topic, subject, message, turnstileToken } = request.body as ContactMessageBody;

    const remoteIp = request.ip;
    const valid = await turnstileClient.verify(turnstileToken, remoteIp);
    if (!valid) {
      return reply.status(400).send({ error: 'Invalid captcha' });
    }

    await zammadClient.createTicket({ name, email, topic, subject, message });
    return reply.status(201).send({});
  });

  server.head('/healthcheck', async (request, reply) => {
    reply.status(200).send();
  });

  try {
    const defaultPort = process.env.NODE_ENV === 'development' ? 3420 : 3000;
    const port = process.env.PORT ? parseInt(process.env.PORT, 10) : defaultPort;
    await server.listen({ host: '0.0.0.0', port });
    console.log(`Server listening on port ${port}`);
  } catch (err) {
    console.error('Failed to start server:', err);
    server.log.error(err);
    process.exit(1);
  }
};

start().catch(err => {
  console.error('Fatal error:', err);
  process.exit(1);
});