import 'reflect-metadata';

import { getEnvFlag } from '@frontmcp/utils';

import { InvalidDecoratorMetadataError } from '../../errors/decorator.errors';
import { InternalMcpError } from '../../errors/mcp.error';
import { frontMcpMetadataSchema, type FrontMcpMetadata } from '../metadata';
import { FrontMcpTokens } from '../tokens';
import { captureCallerDir } from '../utils/caller-dir.utils';

/** Basenames of THIS decorator file, skipped so the captured frame is the user's server module. */
const FRONT_MCP_DECORATOR_BASENAMES = ['front-mcp.decorator.ts', 'front-mcp.decorator.js'] as const;

/**
 * Reflect metadata key under which the @FrontMcp() decorator stores the
 * fully-parsed configuration. Treat as a private implementation detail —
 * external callers should use `getDecoratorConfig(target)` instead so we
 * can change the key or shape without breaking them.
 *
 * @internal
 */
export const FRONTMCP_CONFIG_METADATA_KEY = '__frontmcp:config';

/**
 * Read the @FrontMcp() decorator's parsed config off a class target. Returns
 * `undefined` when the input is not a decorated class — callers should treat
 * that as "no metadata available" and fall back to other sources.
 *
 * Used by the schema extractor and adapter validators to resolve the
 * server's http port, the cloudflare sqlite/redis check, and other
 * build-time decisions without reaching into Reflect directly.
 */
export function getDecoratorConfig(target: unknown): FrontMcpMetadata | undefined {
  if (typeof target !== 'function') return undefined;
  if (typeof Reflect === 'undefined') return undefined;
  const reflect = Reflect as { getMetadata?: (k: string, t: unknown) => unknown };
  if (typeof reflect.getMetadata !== 'function') return undefined;
  const config = reflect.getMetadata(FRONTMCP_CONFIG_METADATA_KEY, target);
  return (config ?? undefined) as FrontMcpMetadata | undefined;
}

// Lazy imports to avoid circular dependency with @frontmcp/sdk package entry point.
// Uses direct relative paths instead of require('@frontmcp/sdk') which would create
// a circular dependency since index.ts exports this decorator.

let _FrontMcpInstance: typeof import('../../front-mcp').FrontMcpInstance | null = null;
function getFrontMcpInstance() {
  if (!_FrontMcpInstance) {
    _FrontMcpInstance = require('../../front-mcp').FrontMcpInstance;
  }
  if (!_FrontMcpInstance) {
    throw new InternalMcpError('FrontMcpInstance not found in module', 'MODULE_LOAD_FAILED');
  }
  return _FrontMcpInstance;
}

// Lazy import for serverless handler functions
type ServerlessHandlerFns = {
  setServerlessHandler: (handler: unknown) => void;
  setServerlessHandlerPromise: (promise: Promise<unknown>) => void;
  setServerlessHandlerError: (error: Error) => void;
};
let _serverlessHandlerFns: ServerlessHandlerFns | null = null;

function getServerlessHandlerFns(): ServerlessHandlerFns {
  if (!_serverlessHandlerFns) {
    _serverlessHandlerFns = require('../../front-mcp/serverless-handler');
  }
  if (!_serverlessHandlerFns) {
    throw new InternalMcpError('Serverless handler functions not found', 'MODULE_LOAD_FAILED');
  }
  return _serverlessHandlerFns;
}

/**
 * Decorator that marks a class as a FrontMcp Server and provides metadata
 */
export function FrontMcp(providedMetadata: FrontMcpMetadata): ClassDecorator {
  // Capture the defining module's directory at decorator-evaluation time (while
  // the user's source file is on the stack), so `auth.ui[slot]` relative paths
  // anchor to THIS config file rather than process.cwd() (#469 / issue #444).
  const sourceDir = captureCallerDir(FRONT_MCP_DECORATOR_BASENAMES);

  return (target: Function) => {
    // Inject the captured dir BEFORE parsing so the schema's `__sourceDir`
    // passthrough preserves it (and a user-supplied value is never overwritten).
    const withSourceDir =
      sourceDir && !(providedMetadata as { __sourceDir?: string }).__sourceDir
        ? ({ ...providedMetadata, __sourceDir: sourceDir } as FrontMcpMetadata)
        : providedMetadata;
    const { error, data: metadata } = frontMcpMetadataSchema.safeParse(withSourceDir);
    if (error) {
      const formatted = error.format();
      if (formatted.apps) {
        throw new InvalidDecoratorMetadataError('FrontMcp', 'apps', JSON.stringify(formatted.apps, null, 2));
      }
      if (formatted.providers) {
        throw new InvalidDecoratorMetadataError('FrontMcp', 'providers', JSON.stringify(formatted.providers, null, 2));
      }
      const loggingFormat = formatted['logging'] as Record<string, unknown> | undefined;
      if (loggingFormat?.['transports']) {
        throw new InvalidDecoratorMetadataError(
          'FrontMcp',
          'logging.transports',
          JSON.stringify(loggingFormat['transports'], null, 2),
        );
      }
      throw error;
    }

    Reflect.defineMetadata(FrontMcpTokens.type, true, target);
    for (const property in metadata) {
      Reflect.defineMetadata(FrontMcpTokens[property] ?? property, metadata[property], target);
    }

    // Store full parsed config for build-time extraction (e.g., schema-extractor via connect()).
    // The metadata key is a private contract — read it via `getDecoratorConfig()` from the SDK,
    // not directly, so the key/shape can change without breaking external callers.
    Reflect.defineMetadata(FRONTMCP_CONFIG_METADATA_KEY, metadata, target);

    const isServerless = getEnvFlag('FRONTMCP_SERVERLESS');
    const isSchemaExtract = getEnvFlag('FRONTMCP_SCHEMA_EXTRACT');
    const isStdio = getEnvFlag('FRONTMCP_STDIO');
    const taskWorkerId = typeof process !== 'undefined' ? process.env?.['FRONTMCP_RUN_TASK_ID'] : undefined;

    if (taskWorkerId && metadata.serve !== false) {
      // Task-worker mode: the parent CliTaskRunner re-invoked us with
      // FRONTMCP_RUN_TASK_ID set. Skip the normal server bootstrap and run
      // the task to completion, then exit.
      void (async () => {
        try {
          const { executeTaskWorker } = await import('../../task/runtime/execute-task.js');
          const code = await executeTaskWorker(metadata, taskWorkerId);
          process.exit(code);
        } catch (err) {
          console.error('[FrontMCP] task worker failed:', err);
          process.exit(2);
        }
      })();
      return;
    }

    if (isSchemaExtract) {
      // Schema extraction mode — metadata already stored above, skip bootstrap
    } else if (isStdio) {
      // Stdio mode (FRONTMCP_STDIO=1): serve over stdin/stdout instead of HTTP.
      // The `--stdio` runner (`node dist/main.js --stdio`) — or the user, before
      // importing the server — sets this flag so the decorated server connects
      // over stdio and never binds a TCP port (#448, #451). `serve: false` opts
      // out of auto-serving entirely, matching the HTTP branch below.
      if (metadata.serve) {
        getFrontMcpInstance()
          .runStdio(metadata)
          .catch((err: unknown) => {
            console.error('[FrontMCP] stdio server failed:', err);
            process.exit(1);
          });
      }
    } else if (isServerless) {
      // Serverless mode: bootstrap, prepare (no listen), store handler globally
      // Uses direct relative imports to avoid circular dependency with @frontmcp/sdk
      const ServerlessInstance = getFrontMcpInstance();
      const { setServerlessHandler, setServerlessHandlerPromise, setServerlessHandlerError } =
        getServerlessHandlerFns();

      const handlerPromise = ServerlessInstance.createHandler(metadata);
      setServerlessHandlerPromise(handlerPromise);
      handlerPromise.then(setServerlessHandler).catch((err: unknown) => {
        const e = err instanceof Error ? err : new InternalMcpError(String(err), 'SERVERLESS_INIT_FAILED');
        setServerlessHandlerError(e);
        console.error('[FrontMCP] Serverless initialization failed:', e);
      });
    } else if (metadata.serve) {
      // Normal mode: bootstrap and start server
      // Use lazy require to avoid circular dependency and dual-package hazard
      getFrontMcpInstance().bootstrap(metadata);
    }
  };
}
