import type { Plugin } from 'vite';
import fs from 'node:fs';
import type { IncomingMessage, ServerResponse } from 'node:http';
import path from 'node:path';
import { spawn } from 'node:child_process';

import {
  fetchHealth,
  normalizeHealthServerInfo,
  readServerInfo,
} from '../scripts/utils/serverInfo.mjs';
import {
  PRODUCT_NAME,
  PROJECT_ID,
  PROJECT_NAME,
  readMakeClientProjectIdentity,
} from '../scripts/sync-project-metadata.mjs';
import { MAKE_CONFIG_RELATIVE_PATH } from './utils/makeConstants';

const DEFAULT_ADMIN_ORIGIN = 'http://localhost:5174';
const STATUS_ROUTE = '/__axhub/make-server/status';
const START_ROUTE = '/__axhub/make-server/start';
const DEFAULT_ADMIN_HEALTH_TIMEOUT_MS = 1200;
const DEFAULT_ADMIN_READY_TIMEOUT_MS = 60000;
const DEFAULT_ADMIN_READY_POLL_INTERVAL_MS = 500;
const SKIP_AUTO_START_SERVER_ENV = 'AXHUB_MAKE_SKIP_AUTO_START_SERVER';

type RegisteredProject = {
  projectId: string;
  projectName: string;
};

type MakeServerStartCommand = {
  command: string;
  args: string[];
  label: string;
};

type MakeServerStatusPayload = {
  ready: boolean;
  starting: boolean;
  adminOrigin: string | null;
  adminUrl: string | null;
  projectId: string;
  projectName: string;
  error?: string;
};

let startPromise: Promise<MakeServerStatusPayload> | null = null;

type ReusableAdminOriginOptions = {
  requireDevMode?: boolean;
  runtimeOrigin?: string;
  healthTimeoutMs?: number;
};

type MakeServerStartOptions = {
  runtimeOrigin?: string;
  adminReadyTimeoutMs?: number;
  pollIntervalMs?: number;
  healthTimeoutMs?: number;
};

type AdminOriginWaitOptions = ReusableAdminOriginOptions & {
  timeoutMs?: number;
  pollIntervalMs?: number;
};

function findWorkspaceRoot(projectRoot: string): string | null {
  let current = path.resolve(projectRoot);
  while (true) {
    if (fs.existsSync(path.join(current, 'pnpm-workspace.yaml'))) {
      return current;
    }
    const parent = path.dirname(current);
    if (parent === current) {
      return null;
    }
    current = parent;
  }
}

function resolveLocalMakeServerCli(projectRoot: string): string | null {
  const workspaceRoot = findWorkspaceRoot(projectRoot);
  if (workspaceRoot) {
    const workspaceCandidates = [
      path.join(workspaceRoot, 'bin/cli.mjs'),
      path.join(workspaceRoot, 'apps/make-server/bin/cli.mjs'),
    ];
    const workspaceCli = workspaceCandidates.find((candidate) => fs.existsSync(candidate));
    if (workspaceCli) {
      return workspaceCli;
    }
  }
  const installedCli = path.join(projectRoot, 'node_modules/@axhub/make/bin/cli.mjs');
  return fs.existsSync(installedCli) ? installedCli : null;
}

/** Dev mode needs the make-server source tree; npm packages only ship dist/admin. */
function canRunMakeServerDevMode(projectRoot: string): boolean {
  const workspaceRoot = findWorkspaceRoot(projectRoot);
  if (workspaceRoot) {
    const workspaceDevIndex = path.join(workspaceRoot, 'src/index/index.html');
    if (fs.existsSync(workspaceDevIndex)) {
      return true;
    }
  }
  const installedDevIndex = path.join(
    projectRoot,
    'node_modules/@axhub/make/src/index/index.html',
  );
  return fs.existsSync(installedDevIndex);
}

function normalizeOrigin(origin: unknown): string | null {
  if (typeof origin !== 'string') {
    return null;
  }
  const trimmed = origin.trim().replace(/\/+$/u, '');
  return trimmed || null;
}

export function resolveMakeServerStartCommand(
  projectRoot: string,
  options: MakeServerStartOptions = {},
): MakeServerStartCommand {
  const runtimeOrigin = normalizeOrigin(options.runtimeOrigin);
  const cliPath = resolveLocalMakeServerCli(projectRoot);
  if (cliPath) {
    const useDevMode = canRunMakeServerDevMode(projectRoot);
    const args = [cliPath, projectRoot];
    if (useDevMode) {
      args.push('--dev');
    }
    if (runtimeOrigin) {
      args.push('--runtime-origin', runtimeOrigin);
    }
    return {
      command: process.execPath,
      args,
      label: useDevMode ? 'local @axhub/make dev' : 'local @axhub/make',
    };
  }
  const args = ['@axhub/make@0.6.4', projectRoot];
  if (runtimeOrigin) {
    args.push('--runtime-origin', runtimeOrigin);
  }
  return {
    command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
    args,
    label: 'npx @axhub/make',
  };
}

export function createAdminUrl(adminOrigin: string, projectId?: string): string {
  const url = new URL('/', adminOrigin);
  const normalizedProjectId = projectId?.trim();
  if (normalizedProjectId) {
    url.searchParams.set('projectId', normalizedProjectId);
  }
  return url.toString();
}

export async function getReusableAdminOrigin(
  projectRoot: string,
  options: ReusableAdminOriginOptions = {},
): Promise<string | null> {
  const info = readServerInfo(projectRoot, 'admin');
  const candidates = [
    info?.origin,
    DEFAULT_ADMIN_ORIGIN,
  ].filter((origin, index, all): origin is string => Boolean(origin) && all.indexOf(origin) === index);

  for (const origin of candidates) {
    const health = await fetchHealth(origin, options.healthTimeoutMs ?? DEFAULT_ADMIN_HEALTH_TIMEOUT_MS);
    if (normalizeHealthServerInfo(health)?.origin && isReusableAdminHealth(health, options)) {
      return origin;
    }
  }
  return null;
}

function isReusableAdminHealth(health: unknown, options: ReusableAdminOriginOptions): boolean {
  if (!options.requireDevMode) {
    return isReusableRuntimeOrigin(health, options);
  }
  if (!health || typeof health !== 'object') {
    return false;
  }
  const payload = health as { role?: unknown; devMode?: unknown };
  return payload.role === 'admin'
    && payload.devMode === true
    && isReusableRuntimeOrigin(health, options);
}

function isReusableRuntimeOrigin(health: unknown, options: ReusableAdminOriginOptions): boolean {
  const expectedRuntimeOrigin = normalizeOrigin(options.runtimeOrigin);
  if (!expectedRuntimeOrigin) {
    return true;
  }
  if (!health || typeof health !== 'object') {
    return false;
  }
  const payload = health as { runtimeOrigin?: unknown };
  return normalizeOrigin(payload.runtimeOrigin) === expectedRuntimeOrigin;
}

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function shouldRequireDevAdmin(projectRoot: string): boolean {
  return canRunMakeServerDevMode(projectRoot) && Boolean(resolveLocalMakeServerCli(projectRoot));
}

export async function waitForAdminOrigin(
  projectRoot: string,
  options: AdminOriginWaitOptions = {},
): Promise<string | null> {
  const timeoutMs = Math.max(0, options.timeoutMs ?? DEFAULT_ADMIN_READY_TIMEOUT_MS);
  const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? DEFAULT_ADMIN_READY_POLL_INTERVAL_MS);
  const startedAt = Date.now();

  while (Date.now() - startedAt <= timeoutMs) {
    const origin = await getReusableAdminOrigin(projectRoot, options);
    if (origin) {
      return origin;
    }
    const elapsedMs = Date.now() - startedAt;
    if (elapsedMs >= timeoutMs) {
      break;
    }
    await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
  }
  return null;
}

export async function registerOfficialProject(projectRoot: string, adminOrigin: string): Promise<RegisteredProject> {
  const resolvedProjectRoot = path.resolve(projectRoot);
  const metadataPath = path.join(resolvedProjectRoot, '.axhub/make/project.json');
  const identity = readMakeClientProjectIdentity(resolvedProjectRoot);
  const body = {
    id: identity.id,
    name: identity.name,
    root: resolvedProjectRoot,
    metadataPath,
  };

  const projectsResponse = await fetch(new URL('/api/projects', adminOrigin), {
    headers: { accept: 'application/json' },
  });
  if (!projectsResponse.ok) {
    throw new Error(`GET /api/projects failed with ${projectsResponse.status}`);
  }
  const registry = await projectsResponse.json() as any;
  const existing = Array.isArray(registry.projects)
    ? registry.projects.find((project: any) => project.id === identity.id || path.resolve(project.root || '') === resolvedProjectRoot)
    : null;

  if (existing) {
    const response = await fetch(new URL(`/api/projects/${encodeURIComponent(existing.id)}`, adminOrigin), {
      method: 'PATCH',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        name: identity.name,
        root: resolvedProjectRoot,
        metadataPath,
      }),
    });
    if (!response.ok) {
      throw new Error(`PATCH /api/projects/${existing.id} failed with ${response.status}`);
    }
  } else {
    const response = await fetch(new URL('/api/projects/make/register-existing', adminOrigin), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ root: resolvedProjectRoot }),
    });
    if (!response.ok && response.status !== 409) {
      throw new Error(`POST /api/projects/make/register-existing failed with ${response.status}`);
    }
  }

  return {
    projectId: existing?.id || identity.id,
    projectName: identity.name,
  };
}

function createStatusPayload(adminOrigin: string | null, registration?: RegisteredProject): MakeServerStatusPayload {
  const projectId = registration?.projectId || PROJECT_ID;
  return {
    ready: Boolean(adminOrigin),
    starting: Boolean(startPromise),
    adminOrigin,
    adminUrl: adminOrigin ? createAdminUrl(adminOrigin, projectId) : null,
    projectId,
    projectName: registration?.projectName || PROJECT_NAME,
  };
}

async function getRegisteredMakeServerStatus(
  projectRoot: string,
  options: MakeServerStartOptions = {},
): Promise<MakeServerStatusPayload> {
  if (startPromise) {
    return {
      ...createStatusPayload(null),
      starting: true,
    };
  }

  const adminOrigin = await getReusableAdminOrigin(projectRoot, {
    requireDevMode: shouldRequireDevAdmin(projectRoot),
    runtimeOrigin: options.runtimeOrigin,
  });
  if (!adminOrigin) {
    return createStatusPayload(null);
  }

  const registration = await registerOfficialProject(projectRoot, adminOrigin);
  return createStatusPayload(adminOrigin, registration);
}

function spawnMakeServer(projectRoot: string, options: MakeServerStartOptions = {}): Promise<void> {
  const startCommand = resolveMakeServerStartCommand(projectRoot, options);
  return new Promise((resolve, reject) => {
    const child = spawn(startCommand.command, startCommand.args, {
      cwd: projectRoot,
      detached: true,
      stdio: 'ignore',
    });
    child.once('error', reject);
    child.once('spawn', () => {
      child.unref();
      console.log(`🚀 Starting Axhub Make server via ${startCommand.label}...`);
      resolve();
    });
  });
}

export async function startOrReuseMakeServer(
  projectRoot: string,
  options: MakeServerStartOptions = {},
): Promise<MakeServerStatusPayload> {
  if (startPromise) {
    return startPromise;
  }

  startPromise = (async () => {
    try {
      const reuseOptions = {
        requireDevMode: shouldRequireDevAdmin(projectRoot),
        runtimeOrigin: options.runtimeOrigin,
        healthTimeoutMs: options.healthTimeoutMs,
      };
      const reusableOrigin = await getReusableAdminOrigin(projectRoot, reuseOptions);
      if (reusableOrigin) {
        const registration = await registerOfficialProject(projectRoot, reusableOrigin);
        return createStatusPayload(reusableOrigin, registration);
      }

      await spawnMakeServer(projectRoot, options);
      const adminOrigin = await waitForAdminOrigin(projectRoot, {
        ...reuseOptions,
        timeoutMs: options.adminReadyTimeoutMs,
        pollIntervalMs: options.pollIntervalMs,
      });
      if (!adminOrigin) {
        return {
          ...createStatusPayload(null),
          error: `Started but did not become ready in time. Open make-server and register ${metadataPathForLog(projectRoot)} manually if needed.`,
        };
      }

      const registration = await registerOfficialProject(projectRoot, adminOrigin);
      return createStatusPayload(adminOrigin, registration);
    } catch (error: any) {
      return {
        ...createStatusPayload(null),
        error: error?.message || String(error),
      };
    } finally {
      startPromise = null;
    }
  })();

  return startPromise;
}

function sendJson(res: ServerResponse, payload: unknown, status = 200) {
  res.statusCode = status;
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
  res.setHeader('Cache-Control', 'no-store');
  res.end(JSON.stringify(payload));
}

function handleMakeServerEndpoint(
  req: IncomingMessage,
  res: ServerResponse,
  projectRoot: string,
  options: MakeServerStartOptions = {},
): boolean {
  const url = new URL(req.url || '/', 'http://localhost');
  if (url.pathname === STATUS_ROUTE) {
    if (req.method !== 'GET') {
      sendJson(res, { error: 'Method not allowed' }, 405);
      return true;
    }
    getRegisteredMakeServerStatus(projectRoot, options)
      .then((payload) => sendJson(res, payload))
      .catch((error: any) => sendJson(res, {
        ...createStatusPayload(null),
        error: error?.message || String(error),
      }));
    return true;
  }

  if (url.pathname === START_ROUTE) {
    if (req.method !== 'POST') {
      sendJson(res, { error: 'Method not allowed' }, 405);
      return true;
    }
    startOrReuseMakeServer(projectRoot, options)
      .then((payload) => sendJson(res, payload))
      .catch((error: any) => sendJson(res, {
        ...createStatusPayload(null),
        error: error?.message || String(error),
      }));
    return true;
  }

  return false;
}

function resolveRuntimeOriginFromViteServer(server: any): string | undefined {
  const actualPort = server.httpServer?.address()?.port || server.config.server?.port;
  if (!actualPort) {
    return undefined;
  }

  const configPath = path.resolve(process.cwd(), MAKE_CONFIG_RELATIVE_PATH);
  let displayHost = 'localhost';
  if (fs.existsSync(configPath)) {
    try {
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      if (typeof config?.server?.host === 'string' && config.server.host.trim()) {
        displayHost = config.server.host.trim();
      }
    } catch {
      // Ignore config parse errors and keep default.
    }
  }

  return `http://${displayHost}:${actualPort}`;
}

export function autoStartMakeServerPlugin(): Plugin {
  return {
    name: 'auto-start-make-server',
    configureServer(server: any) {
      const projectRoot = path.resolve(process.cwd());

      server.middlewares.use((req: IncomingMessage, res: ServerResponse, next: () => void) => {
        if (handleMakeServerEndpoint(req, res, projectRoot, {
          runtimeOrigin: resolveRuntimeOriginFromViteServer(server),
        })) {
          return;
        }
        next();
      });

      if (process.env[SKIP_AUTO_START_SERVER_ENV] === '1') {
        return;
      }

      server.httpServer?.once('listening', async () => {
        try {
          const payload = await startOrReuseMakeServer(projectRoot, {
            runtimeOrigin: resolveRuntimeOriginFromViteServer(server),
          });
          if (payload.ready && payload.adminOrigin) {
            console.log(`✅ Axhub Make server ready at ${payload.adminOrigin}`);
            console.log(`✅ Registered ${payload.projectName || 'unnamed project'} in ${PRODUCT_NAME}; admin URL ${payload.adminUrl}`);
            return;
          }
          console.warn(`[make-server] ${payload.error || `Open make-server and register ${metadataPathForLog(projectRoot)} manually if needed.`}`);
        } catch (error: any) {
          console.warn('[make-server] Auto-start failed:', error?.message || error);
        }
      });
    },
  };
}

function metadataPathForLog(projectRoot: string) {
  return path.join(projectRoot, '.axhub/make/project.json');
}
