import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path';
import AppServerModule from './src/main.server';
import { existsSync } from 'fs';
import * as http from 'http';
import * as https from 'https';
import { CITY, CITY_FROM_SERVER, REQUEST } from './src/app/tokens';
import { toLatinSlug } from './src/app/utils/slug.util';
import { generateLlmsTxt } from './src/app/utils/llms-txt.util';

const CANONICAL_API_URL = process.env['CANONICAL_API_URL'] || 'https://api3.zavedenia.com/v3/';
const CANONICAL_CACHE_TTL_MS = 60 * 60 * 1000;
const STATIC_ASSET_REGEX = /\.(js|css|ico|png|jpg|jpeg|svg|webp|woff2?|ttf|eot|otf|map|txt|xml)(\?.*)?$/i;
const PROFILE_CHILD_ROUTES = new Set([
  'photos',
  'menu',
  'map',
  'reserve',
  'news',
  'comments',
  'videos',
  'events',
  'offers',
]);

interface CacheEntry<T> {
  expiresAt: number;
  value: T;
}

interface CategoryLink {
  id: number;
  link: string;
}

interface ProfileCanonical {
  id: string;
  slug: string;
}

const categoryCache = new Map<string, CacheEntry<CategoryLink>>();
const profileCache = new Map<string, CacheEntry<ProfileCanonical>>();

function getCached<T>(cache: Map<string, CacheEntry<T>>, key: string): T | null {
  const cached = cache.get(key);

  if (!cached) {
    return null;
  }

  if (cached.expiresAt < Date.now()) {
    cache.delete(key);
    return null;
  }

  return cached.value;
}

function setCached<T>(cache: Map<string, CacheEntry<T>>, key: string, value: T): void {
  cache.set(key, {
    expiresAt: Date.now() + CANONICAL_CACHE_TTL_MS,
    value,
  });
}

function requestJson(url: string): Promise<any | null> {
  return new Promise((resolve) => {
    let settled = false;
    const finish = (value: any | null) => {
      if (!settled) {
        settled = true;
        resolve(value);
      }
    };

    const request = https.get(url, { timeout: 2500 }, (response) => {
      if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
        response.resume();
        finish(null);
        return;
      }

      let body = '';
      response.setEncoding('utf8');
      response.on('data', (chunk) => {
        body += chunk;
      });
      response.on('end', () => {
        try {
          finish(JSON.parse(body));
        } catch {
          finish(null);
        }
      });
    });

    request.on('timeout', () => {
      request.destroy();
      finish(null);
    });
    request.on('error', () => finish(null));
  });
}

function normalizeApiBaseUrl(): string {
  return CANONICAL_API_URL.endsWith('/') ? CANONICAL_API_URL : `${CANONICAL_API_URL}/`;
}

function normalizeLinkPath(path: string): string {
  return path.replace(/^\/+/, '').replace(/\/+$/, '');
}

function getFirstHeaderValue(value: string | string[] | undefined): string {
  if (Array.isArray(value)) {
    return value[0] || '';
  }

  return (value || '').split(',')[0].trim();
}

function splitHost(host: string): { hostname: string; port: string } {
  const cleanHost = host.trim();
  const portMatch = cleanHost.match(/:(\d+)$/);
  const port = portMatch ? portMatch[0] : '';
  const hostname = port ? cleanHost.slice(0, -port.length) : cleanHost;

  return { hostname: hostname.toLowerCase(), port };
}

function getRequestHost(req: express.Request): string {
  return getFirstHeaderValue(req.headers['x-forwarded-host']) || req.headers.host || req.hostname || '';
}

function getRequestProtocol(req: express.Request): string {
  return getFirstHeaderValue(req.headers['x-forwarded-proto']) || req.protocol;
}

function getCityFromHost(host: string): string {
  const { hostname } = splitHost(host);
  const domainParts = hostname.split('.');

  if (
    domainParts.length === 2 &&
    domainParts[0] === 'booky' &&
    domainParts[1] === 'mk'
  ) {
    return 'skopje';
  }

  if (
    domainParts.length === 2 &&
    domainParts[0] === 'booknow' &&
    domainParts[1] === 'rs'
  ) {
    return 'beograd';
  }

  if (domainParts.length > 2) {
    return domainParts[0];
  }

  return 'sofia';
}

function getPathAndSearch(originalUrl: string): { path: string; search: string } {
  const queryIndex = originalUrl.indexOf('?');

  if (queryIndex === -1) {
    return { path: originalUrl || '/', search: '' };
  }

  return {
    path: originalUrl.slice(0, queryIndex) || '/',
    search: originalUrl.slice(queryIndex),
  };
}

function buildRedirectTarget(req: express.Request, host: string, path: string, search: string, forceAbsolute: boolean): string {
  if (forceAbsolute) {
    return `${getRequestProtocol(req)}://${host}${path}${search}`;
  }

  return `${path}${search}`;
}

async function getCanonicalCategoryLink(city: string, language: string, id: number): Promise<CategoryLink | null> {
  const key = `${city}:${language}:${id}`;
  const cached = getCached(categoryCache, key);

  if (cached) {
    return cached;
  }

  const data = await requestJson(`${normalizeApiBaseUrl()}venuesType/${city}/${language}`);
  const groups = ['types', 'menu', 'special', 'occasion'];

  for (const group of groups) {
    const category = data?.[group]?.find((item: any) => Number(item?.id) === id);

    if (category?.link_type) {
      const value = {
        id,
        link: normalizeLinkPath(String(category.link_type)),
      };
      setCached(categoryCache, key, value);
      return value;
    }
  }

  return null;
}

async function getCanonicalProfile(city: string, language: string, id: string): Promise<ProfileCanonical | null> {
  const canonicalId = id === '4396' ? '4002' : id;
  const key = `${city}:${language}:${canonicalId}`;
  const cached = getCached(profileCache, key);

  if (cached) {
    return cached;
  }

  const data = await requestJson(`${normalizeApiBaseUrl()}profile/${city}/${canonicalId}/${language}/`);
  const profile = data?.profile;
  const name = profile?.zavedenie || profile?.name || '';

  if (!profile?.id || !name) {
    return null;
  }

  const value = {
    id: String(profile.id),
    slug: toLatinSlug(String(name)),
  };

  if (!value.slug) {
    return null;
  }

  setCached(profileCache, key, value);
  return value;
}

async function resolveCategoryRedirect(path: string, city: string, language: string): Promise<string | null> {
  const segments = path.split('/').filter(Boolean);
  const hasLanguagePrefix = segments[0] === 'en';
  const categoryIndex = hasLanguagePrefix ? 1 : 0;
  const categorySegment = segments[categoryIndex];
  const categoryMatch = categorySegment?.match(/^(.+)_(\d+)$/);

  if (!categoryMatch || segments.length > categoryIndex + 2) {
    return null;
  }

  const categoryId = Number(categoryMatch[2]);
  const nextSegment = segments[categoryIndex + 1];

  if (nextSegment && nextSegment !== 'map' && !/^\d+$/.test(nextSegment)) {
    return null;
  }

  const canonicalCategory = await getCanonicalCategoryLink(city, language, categoryId);

  if (!canonicalCategory?.link) {
    return null;
  }

  const targetSegments = hasLanguagePrefix ? ['en', canonicalCategory.link] : [canonicalCategory.link];

  if (nextSegment === 'map') {
    targetSegments.push('map');
  } else if (nextSegment && nextSegment !== '1') {
    targetSegments.push(nextSegment);
  }

  const targetPath = `/${targetSegments.join('/')}`;
  return targetPath === path ? null : targetPath;
}

async function resolveProfileRedirect(path: string, city: string, language: string): Promise<string | null> {
  const segments = path.split('/').filter(Boolean);
  const hasLanguagePrefix = segments[0] === 'en';
  const profileIndex = hasLanguagePrefix ? 1 : 0;
  const profileId = segments[profileIndex];

  if (!profileId || !/^\d{1,4}$/.test(profileId)) {
    return null;
  }

  const canonicalProfile = await getCanonicalProfile(city, language, profileId);

  if (!canonicalProfile) {
    return null;
  }

  const maybeSlug = segments[profileIndex + 1];
  const hasSlug = Boolean(maybeSlug) && !PROFILE_CHILD_ROUTES.has(maybeSlug);
  const childStartIndex = hasSlug ? profileIndex + 2 : profileIndex + 1;
  const childSegments = segments.slice(childStartIndex);
  const targetSegments = hasLanguagePrefix
    ? ['en', canonicalProfile.id, canonicalProfile.slug, ...childSegments]
    : [canonicalProfile.id, canonicalProfile.slug, ...childSegments];
  const targetPath = `/${targetSegments.join('/')}`;

  return targetPath === path ? null : targetPath;
}

async function resolveCanonicalPath(path: string, city: string, language: string): Promise<string | null> {
  const categoryRedirect = await resolveCategoryRedirect(path, city, language);

  if (categoryRedirect) {
    return categoryRedirect;
  }

  return resolveProfileRedirect(path, city, language);
}

function isCanonicalRedirectCandidate(req: express.Request, path: string): boolean {
  if (req.method !== 'GET' && req.method !== 'HEAD') {
    return false;
  }

  return !STATIC_ASSET_REGEX.test(path);
}


export function app(): express.Express {
  const server = express();


  server.use(express.json());


  server.use((req, res, next) => {
    console.log(
      `Incoming request: ${req.method} ${req.url} | User-Agent: ${req.headers['user-agent'] || 'N/A'
      }`,
    );
    next();
  });

  server.set('trust proxy', 1);

  server.use((req, res, next) => {
    const requestHost = getRequestHost(req);
    const hostParts = splitHost(requestHost);
    const canonicalHost = hostParts.hostname.startsWith('www.')
      ? `${hostParts.hostname.replace(/^www\./, '')}${hostParts.port}`
      : requestHost;
    const hostChanged = canonicalHost !== requestHost;
    const { path, search } = getPathAndSearch(req.originalUrl);

    if (!isCanonicalRedirectCandidate(req, path)) {
      if (hostChanged) {
        return res.redirect(301, buildRedirectTarget(req, canonicalHost, path, search, true));
      }

      return next();
    }

    let canonicalPath = path;
    let pathChanged = false;

    if (canonicalPath.length > 1 && canonicalPath.endsWith('/')) {
      canonicalPath = canonicalPath.replace(/\/+$/, '') || '/';
      pathChanged = canonicalPath !== path;
    }

    const city = getCityFromHost(canonicalHost);
    const language = canonicalPath.split('/').filter(Boolean)[0] === 'en' ? 'en' : 'bg';

    resolveCanonicalPath(canonicalPath, city, language)
      .then((resolvedPath) => {
        if (resolvedPath && resolvedPath !== canonicalPath) {
          canonicalPath = resolvedPath;
          pathChanged = true;
        }

        if (hostChanged || pathChanged) {
          return res.redirect(301, buildRedirectTarget(req, canonicalHost, canonicalPath, search, hostChanged));
        }

        return next();
      })
      .catch((err) => {
        console.warn('Canonical redirect lookup failed:', err);

        if (hostChanged || pathChanged) {
          return res.redirect(301, buildRedirectTarget(req, canonicalHost, canonicalPath, search, hostChanged));
        }

        return next();
      });
  });

  // Disable caching only for HTML (not JS / assets)
  server.use((req, res, next) => {
    const accept = req.headers.accept || '';
    if (accept.includes('text/html')) {
      res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
      res.setHeader('Pragma', 'no-cache');
      res.setHeader('Expires', '0');
    }
    next();
  });

  const serverDistFolder = dirname(fileURLToPath(import.meta.url));
  const browserDistFolder = process.env['BROWSER_DIST_FOLDER']
    ? process.env['BROWSER_DIST_FOLDER']
    : resolve(serverDistFolder, '../browser');

  const indexHtml = existsSync(join(browserDistFolder, 'index.csr.html'))
    ? join(browserDistFolder, 'index.csr.html')
    : join(browserDistFolder, 'index.html');

  console.log('Server dist folder:', serverDistFolder);
  console.log('Browser dist folder:', browserDistFolder);
  console.log('Index HTML path:', indexHtml);

  const commonEngine = new CommonEngine();

  server.set('view engine', 'html');
  server.set('views', browserDistFolder);

  server.get('/favicon.ico', (req, res) => {
    const host = req.headers.host || '';
    const domainParts = host.split('.');
    let country = 'bg'; // default

    if (domainParts.length === 2 && domainParts[0].toLowerCase() === 'booky' && domainParts[1].toLowerCase() === 'mk') {
      country = 'mk';
    } else if (domainParts.length === 2 && domainParts[0].toLowerCase() === 'booknow' && domainParts[1].toLowerCase() === 'rs') {
      country = 'rs';
    } else if (domainParts.length > 2) {
      country = domainParts[0].toLowerCase();
    }

    const faviconPath = (country === 'mk' || country === 'rs')
      ? join(browserDistFolder, 'logoB.ico')
      : join(browserDistFolder, 'favicon.ico');

    if (existsSync(faviconPath)) {
      res.sendFile(faviconPath);
    } else {
      res.sendFile(join(browserDistFolder, 'favicon.ico'));
    }
  });

  server.get('/favicon.png', (req, res) => {
    const host = req.headers.host || '';
    const domainParts = host.split('.');
    let country = 'bg'; // default

    if (domainParts.length === 2 && domainParts[0].toLowerCase() === 'booky' && domainParts[1].toLowerCase() === 'mk') {
      country = 'mk';
    } else if (domainParts.length === 2 && domainParts[0].toLowerCase() === 'booknow' && domainParts[1].toLowerCase() === 'rs') {
      country = 'rs';
    } else if (domainParts.length > 2) {
      country = domainParts[0].toLowerCase();
    }

    const faviconPath = (country === 'mk' || country === 'rs')
      ? join(browserDistFolder, 'logoB.png')
      : join(browserDistFolder, 'favicon.png');

    if (existsSync(faviconPath)) {
      res.sendFile(faviconPath);
    } else {
      res.sendFile(join(browserDistFolder, 'favicon.png'));
    }
  });

  server.get('/llms.txt', (req, res) => {
    const host = getRequestHost(req);
    const city = getCityFromHost(host);

    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.setHeader('Cache-Control', 'public, max-age=3600');
    res.status(200).send(generateLlmsTxt(city));
  });

  server.get('*.*', express.static(browserDistFolder, { maxAge: '1y' }));

  // 🔴 Endpoint to receive client-side JS errors
  server.post('/client-error', (req, res) => {
    console.error('🚨 CLIENT ERROR:', JSON.stringify(req.body, null, 2));
    res.status(204).end();
  });

  // ✅ SSR route
  server.get('*', (req, res, next) => {
    const url = req.url;
    const userAgent = req.headers['user-agent'] || '';

    // Skip SSR for static assets (already handled above)
    if (
      url.match(
        /\.(js|css|ico|png|jpg|jpeg|svg|webp|woff2?|ttf|eot|otf|map)(\?.*)?$/i,
      )
    ) {
      return next();
    }

    // Skip SSR for obviously bad/suspicious paths
    if (url.match(/\.(s?html?|php|asp|aspx|cgi)$/i)) {
      console.warn(`❌ Blocked SSR for suspicious URL: ${url}`);
      return res.status(404).send('Not Found');
    }

    const blockedBots = [
      'SemrushBot',
      'DotBot',
      'MJ12bot',
      'AhrefsBot',
      'crawler',
      'spider',
      'curl',
      'wget',
      'python-requests',
    ];

    const isBlockedBot = blockedBots.some((bot) =>
      userAgent.toLowerCase().includes(bot.toLowerCase()),
    );

    if (isBlockedBot) {
      console.warn(`🚫 Blocked SSR for aggressive bot: ${userAgent}`);
      return res
        .status(200)
        .send(
          '<!doctype html><html><head><meta name="robots" content="noindex" /></head><body><p>Bot SSR disabled</p></body></html>',
        );
    }

    const host = req.headers.host || '';
    console.log('Host header:', host);
    const domainParts = host.split('.');
    console.log('Domain parts:', domainParts);

    let city = 'sofia'; // default

    if (
      domainParts.length === 2 &&
      domainParts[0].toLowerCase() === 'booky' &&
      domainParts[1].toLowerCase() === 'mk'
    ) {
      city = 'skopje';
    } else if (
      domainParts.length === 2 &&
      domainParts[0].toLowerCase() === 'booknow' &&
      domainParts[1].toLowerCase() === 'rs'
    ) {
      city = 'beograd';
    } else if (domainParts.length > 2) {
      city = domainParts[0].toLowerCase();
    }
    console.log('Host:', host);
    console.log('Extracted City:', city);

    const ssrStart = Date.now();

    commonEngine
      .render({
        bootstrap: AppServerModule,
        documentFilePath: indexHtml,
        url: req.originalUrl,
        publicPath: browserDistFolder,
        providers: [
          { provide: REQUEST, useValue: req },
          { provide: APP_BASE_HREF, useValue: req.baseUrl },
          { provide: CITY, useValue: city },
          { provide: CITY_FROM_SERVER, useValue: city },
        ],
      })
      .then((html) => {
        const duration = Date.now() - ssrStart;
        console.log(
          `SSR rendered ${url} in ${duration}ms, html length: ${html?.length ?? 0
          }`,
        );

        if (!html || html.trim().length < 100) {
          console.warn(
            `⚠️ Empty or short HTML for ${url}, falling back to simple page`,
          );
          return res
            .status(200)
            .send(
              '<!doctype html><html><body><h1>Empty SSR output</h1></body></html>',
            );
        }

        const isBooky = (city === 'skopje' || city === 'beograd');
        const faviconHref = isBooky ? '/logoB.png' : '/favicon.png';
        const appleTouchHref = isBooky ? '/logoB.png' : '/favicon.png';

        html = html.replace(
          /<link[^>]*rel=["']icon["'][^>]*>/i,
          `<link rel="icon" type="image/png" href="${faviconHref}">`
        );

        html = html.replace(
          /<link[^>]*rel=["']apple-touch-icon["'][^>]*>/i,
          `<link rel="apple-touch-icon" sizes="180x180" href="${appleTouchHref}">`
        );

        console.log(`✅ SSR success: ${url} (favicon: ${faviconHref})`);
        res.send(html);
      })
      .catch((err) => {
        console.error(`❌ SSR error for ${url}:`, err);
        res.status(500).send('SSR server error');
      });
  });

  return server;
}

function run(): void {
  const port = Number(process.env['PORT'] || 4000);
  const expressApp = app();

  const server = http.createServer(expressApp);

  server.keepAliveTimeout = 5000;
  server.headersTimeout = 6000;
  server.setTimeout(10000);

  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });

  server.on('connection', (socket) => {
    socket.setTimeout(10000);
    socket.on('timeout', () => {
      socket.destroy();
    });
  });
}

run();
