import { execSync } from 'node:child_process';
import type { ServiceConfig, CheckResult, ContentCheck, ApiEndpointCheck } from './types.js';
import type { Logger } from './logger.js';
import { getKnownMissingEndpoints, markEndpointDone } from './dev-backlog.js';

let cachedPm2List: Array<{ name: string; pm2_env: { status: string } }> | null = null;

export function resetPm2Cache(): void {
  cachedPm2List = null;
}

function getPm2List(logger: Logger): typeof cachedPm2List {
  if (cachedPm2List !== null) return cachedPm2List;

  try {
    const raw = execSync('pm2 jlist', { timeout: 10_000, encoding: 'utf-8' });
    cachedPm2List = JSON.parse(raw) as typeof cachedPm2List;
    return cachedPm2List;
  } catch (err) {
    logger.error('Failed to get PM2 list', { error: String(err) });
    return null;
  }
}

async function checkHttp(service: ServiceConfig, _logger: Logger): Promise<CheckResult> {
  const start = Date.now();
  const url = service.url!;
  const timeout = service.timeoutMs ?? 10_000;
  const expected = service.expectedStatusCodes ?? [200];

  try {
    const response = await fetch(url, {
      signal: AbortSignal.timeout(timeout),
      redirect: 'manual',
    });
    const elapsed = Date.now() - start;
    const ok = expected.includes(response.status);

    return {
      serviceId: service.id,
      serviceName: service.name,
      ok,
      responseTimeMs: elapsed,
      details: `HTTP ${response.status}`,
      error: ok ? undefined : `Unexpected status ${response.status}`,
      timestamp: new Date(),
    };
  } catch (err) {
    const elapsed = Date.now() - start;
    const errorMsg = err instanceof Error ? err.message : String(err);

    return {
      serviceId: service.id,
      serviceName: service.name,
      ok: false,
      responseTimeMs: elapsed,
      error: errorMsg,
      timestamp: new Date(),
    };
  }
}

function checkPm2(service: ServiceConfig, logger: Logger): CheckResult {
  const start = Date.now();
  const pm2Name = service.pm2Name!;
  const list = getPm2List(logger);

  if (!list) {
    return {
      serviceId: service.id,
      serviceName: service.name,
      ok: false,
      responseTimeMs: Date.now() - start,
      error: 'Failed to query PM2',
      timestamp: new Date(),
    };
  }

  const proc = list.find((p) => p.name === pm2Name);

  if (!proc) {
    return {
      serviceId: service.id,
      serviceName: service.name,
      ok: false,
      responseTimeMs: Date.now() - start,
      error: `Process "${pm2Name}" not found in PM2`,
      timestamp: new Date(),
    };
  }

  const status = proc.pm2_env?.status;
  const isOnline = status === 'online';

  return {
    serviceId: service.id,
    serviceName: service.name,
    ok: isOnline,
    responseTimeMs: Date.now() - start,
    details: `PM2 status: ${status}`,
    error: isOnline ? undefined : `PM2 status: ${status}`,
    timestamp: new Date(),
  };
}

function checkDockerPg(service: ServiceConfig): CheckResult {
  const start = Date.now();
  const container = service.containerName ?? 'fitcrm-postgres';

  try {
    execSync(`docker exec ${container} pg_isready -U fitcrm -d fitcrm`, {
      timeout: 5_000,
      encoding: 'utf-8',
    });
    return {
      serviceId: service.id,
      serviceName: service.name,
      ok: true,
      responseTimeMs: Date.now() - start,
      details: 'pg_isready OK',
      timestamp: new Date(),
    };
  } catch (err) {
    return {
      serviceId: service.id,
      serviceName: service.name,
      ok: false,
      responseTimeMs: Date.now() - start,
      error: err instanceof Error ? err.message : String(err),
      timestamp: new Date(),
    };
  }
}

function checkDockerRedis(service: ServiceConfig): CheckResult {
  const start = Date.now();
  const container = service.containerName ?? 'fitcrm-redis';
  const password = process.env['REDIS_PASSWORD'] ?? 'redis_dev_password';

  try {
    const output = execSync(`docker exec ${container} redis-cli -a ${password} ping 2>/dev/null`, {
      timeout: 5_000,
      encoding: 'utf-8',
    }).trim();

    const ok = output === 'PONG';
    return {
      serviceId: service.id,
      serviceName: service.name,
      ok,
      responseTimeMs: Date.now() - start,
      details: ok ? 'PONG' : `Unexpected: ${output}`,
      error: ok ? undefined : `Expected PONG, got: ${output}`,
      timestamp: new Date(),
    };
  } catch (err) {
    return {
      serviceId: service.id,
      serviceName: service.name,
      ok: false,
      responseTimeMs: Date.now() - start,
      error: err instanceof Error ? err.message : String(err),
      timestamp: new Date(),
    };
  }
}

async function checkContent(
  service: ServiceConfig,
  contentCheck: ContentCheck,
  _logger: Logger,
): Promise<CheckResult> {
  const start = Date.now();

  try {
    // Fetch the page HTML (follow redirects to get final content)
    const response = await fetch(contentCheck.url, {
      signal: AbortSignal.timeout(15_000),
      redirect: 'follow',
    });
    const html = await response.text();
    const elapsed = Date.now() - start;

    // Check 1: Look for bad patterns in the visible HTML body
    // Only check the initial server-rendered HTML (before <script> tags)
    const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)(?=<script)/);
    const bodyContent = bodyMatch?.[1] ?? html;

    for (const pattern of contentCheck.badPatterns) {
      if (bodyContent.includes(pattern)) {
        // Verify it's the ONLY visible content (stuck loading screen)
        // Remove HTML tags to get visible text
        const visibleText = bodyContent
          .replace(/<[^>]*>/g, '')
          .replace(/\s+/g, ' ')
          .trim();

        // If the only visible text is the bad pattern, it's a stuck page
        if (visibleText === pattern || visibleText.length < pattern.length + 20) {
          const pagePath = new URL(contentCheck.url).pathname;
          return {
            serviceId: service.id,
            serviceName: service.name,
            ok: false,
            responseTimeMs: elapsed,
            error: `Страница ${pagePath} зависла на "${pattern}" — возможно сломан JS/кэш`,
            details: `Content check: stuck on "${pattern}" (page: ${pagePath})`,
            timestamp: new Date(),
          };
        }
      }
    }

    // Check 2: Verify JS chunks are accessible
    if (contentCheck.verifyChunks) {
      const chunkPattern = /src="(\/_next\/static\/chunks\/[^"]+)"/g;
      const chunks: string[] = [];
      let match;
      while ((match = chunkPattern.exec(html)) !== null) {
        chunks.push(match[1]);
      }

      // Only check app-specific chunks (layout.js, page.js)
      // Skip framework chunks (main-app, webpack, polyfills, app-pages-internals)
      const appChunks = chunks.filter(
        (c) => c.includes('/app/') && !c.includes('app-pages-internals'),
      );

      const baseUrl = new URL(contentCheck.url).origin;

      for (const chunk of appChunks) {
        // Strip query string for cleaner error messages
        const chunkUrl = `${baseUrl}${chunk}`;
        try {
          const chunkResp = await fetch(chunkUrl, {
            signal: AbortSignal.timeout(5_000),
          });

          if (chunkResp.status === 404) {
            const chunkName = chunk.split('/').pop()?.split('?')[0] ?? chunk;
            const pagePath = new URL(contentCheck.url).pathname;
            return {
              serviceId: service.id,
              serviceName: service.name,
              ok: false,
              responseTimeMs: Date.now() - start,
              error: `JS-чанк недоступен (404): ${chunkName} на странице ${pagePath} — протухший .next кэш`,
              details: `Chunk 404: ${chunk} (page: ${pagePath})`,
              timestamp: new Date(),
            };
          }
        } catch {
          // Chunk fetch failed — not critical, might be a timing issue
        }
      }
    }

    return {
      serviceId: service.id,
      serviceName: service.name,
      ok: true,
      responseTimeMs: elapsed,
      details: 'Content check OK',
      timestamp: new Date(),
    };
  } catch (err) {
    return {
      serviceId: service.id,
      serviceName: service.name,
      ok: false,
      responseTimeMs: Date.now() - start,
      error: `Content check failed: ${err instanceof Error ? err.message : String(err)}`,
      timestamp: new Date(),
    };
  }
}

export interface ApiEndpointIssue {
  endpoint: string;
  page: string;
  status: number;
  error?: string;
}

export interface ApiCheckResult {
  newMissing: ApiEndpointIssue[]; // 404 — not implemented (dev task)
  knownMissing: string[]; // 404 — already in backlog
  serverErrors: ApiEndpointIssue[]; // 500 — backend crash (operational)
  forbidden: ApiEndpointIssue[]; // 403 — wrong permissions (dev task)
  resolved: string[];
  total: number;
}

/** Authenticate with the API and return a JWT token */
async function getApiToken(
  baseUrl: string,
  auth: { phone: string; password: string },
  logger: Logger,
): Promise<string | null> {
  try {
    const resp = await fetch(`${baseUrl}/v1/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ phone: auth.phone, password: auth.password }),
      signal: AbortSignal.timeout(5_000),
    });

    if (!resp.ok) {
      logger.warn('API auth failed', { status: resp.status });
      return null;
    }

    const data = (await resp.json()) as { accessToken?: string };
    return data.accessToken ?? null;
  } catch (err) {
    logger.warn('API auth error', { error: err instanceof Error ? err.message : String(err) });
    return null;
  }
}

export async function checkApiEndpoints(
  service: ServiceConfig,
  endpoints: ApiEndpointCheck[],
  logger: Logger,
): Promise<ApiCheckResult> {
  const baseUrl = service.apiBaseUrl ?? 'http://localhost:3000';
  const known = getKnownMissingEndpoints(service.id);
  const newMissing: ApiEndpointIssue[] = [];
  const stillMissing: string[] = [];
  const serverErrors: ApiEndpointIssue[] = [];
  const forbidden: ApiEndpointIssue[] = [];
  const resolved: string[] = [];

  // Authenticate if credentials are available
  let token: string | null = null;
  if (service.apiAuth) {
    token = await getApiToken(baseUrl, service.apiAuth, logger);
  }

  for (const ep of endpoints) {
    try {
      const headers: Record<string, string> = {};
      if (token) {
        headers['Authorization'] = `Bearer ${token}`;
      }

      const resp = await fetch(`${baseUrl}${ep.endpoint}`, {
        signal: AbortSignal.timeout(5_000),
        headers,
      });

      const isOk = resp.status >= 200 && resp.status < 300;
      const isAlreadyKnown = known.has(ep.endpoint);

      if (isOk) {
        // Endpoint works
        if (isAlreadyKnown) {
          resolved.push(ep.endpoint);
          markEndpointDone(ep.endpoint, logger);
          logger.info(`API endpoint fixed: ${ep.endpoint}`);
        }
        continue;
      }

      // Already tracked in backlog — don't report again
      if (isAlreadyKnown) {
        stillMissing.push(ep.endpoint);
        continue;
      }

      // Parse error message from response body
      let errorMsg: string | undefined;
      try {
        const body = (await resp.json()) as { message?: string };
        errorMsg = body.message;
      } catch {
        /* ignore */
      }

      if (resp.status === 404) {
        newMissing.push({ endpoint: ep.endpoint, page: ep.page, status: 404 });
      } else if (resp.status >= 500) {
        serverErrors.push({
          endpoint: ep.endpoint,
          page: ep.page,
          status: resp.status,
          error: errorMsg ?? `HTTP ${resp.status}`,
        });
      } else if (resp.status === 403) {
        forbidden.push({
          endpoint: ep.endpoint,
          page: ep.page,
          status: 403,
          error: errorMsg ?? 'Forbidden',
        });
      } else if (resp.status === 400) {
        serverErrors.push({
          endpoint: ep.endpoint,
          page: ep.page,
          status: 400,
          error: errorMsg ?? 'Bad Request',
        });
      }
      // 401 without auth = endpoint exists but requires auth — OK
    } catch {
      // Network error — skip, HTTP check will catch connectivity issues
    }
  }

  return {
    newMissing,
    knownMissing: stillMissing,
    serverErrors,
    forbidden,
    resolved,
    total: endpoints.length,
  };
}

export async function checkService(service: ServiceConfig, logger: Logger): Promise<CheckResult[]> {
  const results: CheckResult[] = [];

  if (service.type === 'http' || service.url) {
    results.push(await checkHttp(service, logger));
  }

  if (service.pm2Name) {
    results.push(checkPm2(service, logger));
  }

  if (service.type === 'docker-pg') {
    results.push(checkDockerPg(service));
  }

  if (service.type === 'docker-redis') {
    results.push(checkDockerRedis(service));
  }

  if (service.contentCheck) {
    results.push(await checkContent(service, service.contentCheck, logger));
  }

  // Check internal (auth-gated) pages for broken JS chunks
  if (service.internalPages) {
    for (const page of service.internalPages) {
      results.push(await checkContent(service, page, logger));
    }
  }

  // API endpoint checks are handled separately — they create dev tasks, not failures
  return results;
}

export async function runAllChecks(
  services: ServiceConfig[],
  logger: Logger,
): Promise<Map<string, CheckResult>> {
  resetPm2Cache();

  const aggregated = new Map<string, CheckResult>();

  for (const service of services) {
    const results = await checkService(service, logger);

    // Aggregate: service is OK only if ALL its checks pass
    const allOk = results.every((r) => r.ok);
    const failedResult = results.find((r) => !r.ok);
    const maxResponseTime = Math.max(...results.map((r) => r.responseTimeMs));

    aggregated.set(service.id, {
      serviceId: service.id,
      serviceName: service.name,
      ok: allOk,
      responseTimeMs: maxResponseTime,
      error: failedResult?.error,
      details: results
        .map((r) => r.details)
        .filter(Boolean)
        .join('; '),
      timestamp: new Date(),
    });
  }

  return aggregated;
}
