import { BaseService, type BaseServiceContext } from '@learnitive/base-utils';
import { SERVICE_NAME } from '@learnitive/constants';
import {
  canonicalProfileForMode,
  GENERATED_DIRECTORY_EXCLUSIONS,
  isRunnableSnippetMode,
  projectModeById,
  resolveOperation,
  type ProjectOperation,
  type ProjectProfile,
  type UniversalExecutionResult,
} from '@learnitive/project-runtime';
import type { ProjectStorageService } from './project-storage-service';

const MAX_SNAPSHOT_FILES = 450;
const MAX_SNAPSHOT_BYTES = 25_000_000;
const MAX_SINGLE_FILE_BYTES = 10_000_000;

const SAFE_RUNTIME_ERRORS = new Set([
  'runtime_image_missing',
  'runtime_workspace_denied',
  'runtime_docker_unavailable',
  'runtime_container_failed',
  'runtime_timeout',
  'runtime_language_unsupported',
  'runtime_dependency_missing',
  'runtime_dependency_unsupported',
  'runtime_internal_error',
  'runtime_cancelled',
  'runtime_request_too_large',
  'runtime_queue_unavailable',
  'runtime_queue_full',
  'runtime_compile_failed',
  'runtime_execution_failed',
  'runtime_resource_limit',
  'runtime_preview_missing',
]);

function excluded(path: string): boolean {
  const segments = path.replaceAll('\\', '/').split('/').filter(Boolean);
  return segments.some((segment) => GENERATED_DIRECTORY_EXCLUSIONS.includes(segment));
}

function base64(bytes: Uint8Array): string {
  let binary = '';
  const chunk = 0x8000;
  for (let offset = 0; offset < bytes.length; offset += chunk) {
    binary += String.fromCharCode(...bytes.subarray(offset, Math.min(offset + chunk, bytes.length)));
  }
  return btoa(binary);
}

function createExecutionId(): string {
  if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID();
  return `exec-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
}

async function csrfToken(signal?: AbortSignal): Promise<string> {
  const response = await fetch('/api/auth/me', { credentials: 'same-origin', cache: 'no-store', signal });
  const payload = await response.json().catch(() => ({})) as { authenticated?: boolean; csrfToken?: string; error?: string };
  if (!response.ok || !payload.authenticated || !payload.csrfToken) throw new Error(payload.error || 'Sign in to use Learnitive cloud execution.');
  return payload.csrfToken;
}

export class ProjectExecutionError extends Error {
  readonly code: string;
  readonly executionId?: string;
  readonly stage?: string;
  readonly statusCode?: number;

  constructor(code: string, options: { executionId?: string; stage?: string; statusCode?: number } = {}) {
    const reference = options.executionId ? ` Reference: ${options.executionId}.` : '';
    super(`${runtimeErrorMessage(code)}${reference}`);
    this.name = 'ProjectExecutionError';
    this.code = code;
    this.executionId = options.executionId;
    this.stage = options.stage;
    this.statusCode = options.statusCode;
  }
}

export function runtimeErrorMessage(code: string): string {
  return ({
    runtime_image_missing: 'The execution image is not installed on this Learnitive runtime.',
    runtime_workspace_denied: 'The runtime could not access the isolated project workspace.',
    runtime_docker_unavailable: 'The isolated execution service is unavailable.',
    runtime_container_failed: 'The isolated execution container could not start.',
    runtime_timeout: 'Execution exceeded the allowed time limit.',
    runtime_language_unsupported: 'This project language or operation is not supported by the selected runtime.',
    runtime_dependency_missing: 'A required runtime/compiler dependency is missing from the execution image.',
    runtime_dependency_unsupported: 'This project declares dependencies that are not allowed by the trusted runtime profile.',
    runtime_cancelled: 'Execution was cancelled.',
    runtime_request_too_large: 'The project snapshot is too large for cloud execution.',
    runtime_queue_unavailable: 'The execution queue is temporarily unavailable.',
    runtime_queue_full: 'The execution service is busy. Please try again shortly.',
    runtime_compile_failed: 'The project could not be compiled.',
    runtime_execution_failed: 'The program exited with an error.',
    runtime_resource_limit: 'Execution exceeded an allowed resource limit.',
    runtime_preview_missing: 'The web project built successfully but did not produce a preview entry file.',
    runtime_internal_error: 'Learnitive could not complete the runtime request.',
  } as Record<string, string>)[code] ?? 'Learnitive could not complete the runtime request.';
}

export class ProjectExecutionService extends BaseService {
  static deps = ['projectStorage'] as const;
  private readonly stdinByWorkspace = new Map<string, string>();

  constructor(
    context: BaseServiceContext,
    private readonly dependencies: { projectStorage: ProjectStorageService },
  ) {
    super(SERVICE_NAME.projectExecutionService, context, dependencies);
  }

  hookMount(): void {
    this.addCleanup(() => this.stdinByWorkspace.clear());
  }

  getStdin(wsName: string): string {
    return this.stdinByWorkspace.get(wsName) ?? '';
  }

  setStdin(wsName: string, stdin: string): void {
    this.stdinByWorkspace.set(wsName, String(stdin).slice(0, 1_000_000));
  }

  async resolveProfile(wsName: string, activePath?: string): Promise<ProjectProfile> {
    const selected = await this.dependencies.projectStorage.getProjectProfile(wsName);
    if (selected) return selected;
    const targets = await this.dependencies.projectStorage.detectProjectTargets(wsName, { activePath });
    if (!targets[0]) throw new Error('Learnitive could not detect a runnable project target. Add .learnitive.yml or choose a project profile.');
    const detected = targets[0].profile;
    return this.dependencies.projectStorage.updateProjectProfile(wsName, detected).catch(() => detected);
  }

  async snapshot(wsName: string, signal?: AbortSignal): Promise<Array<{ path: string; contentBase64: string }>> {
    const provider = await this.dependencies.projectStorage.getProvider(wsName);
    const entries = (await provider.listEntries(signal)).filter((entry) => entry.kind === 'file' && !excluded(String(entry.path)));
    if (entries.length > MAX_SNAPSHOT_FILES) throw new Error(`Project snapshot has ${entries.length} files; limit is ${MAX_SNAPSHOT_FILES}.`);
    const files: Array<{ path: string; contentBase64: string }> = [];
    let total = 0;
    for (const entry of entries) {
      signal?.throwIfAborted();
      if (entry.size > MAX_SINGLE_FILE_BYTES) throw new Error(`Project file ${entry.path} is too large for a stateless snapshot.`);
      const bytes = await provider.readFile(entry.path, signal);
      if (!bytes) continue;
      total += bytes.byteLength;
      if (total > MAX_SNAPSHOT_BYTES) throw new Error(`Project snapshot exceeds the ${MAX_SNAPSHOT_BYTES} byte limit.`);
      files.push({ path: String(entry.path), contentBase64: base64(bytes) });
    }
    return files;
  }

  private async executePrepared(
    projectKey: string,
    operation: ProjectOperation,
    provider: NonNullable<ProjectProfile['providers'][ProjectOperation]>,
    files: Array<{ path: string; contentBase64: string }>,
    entry: string | undefined,
    command: string | undefined,
    options: { artifactPaths?: string[]; stdin?: string; signal?: AbortSignal; onStatus?: (status: 'queued' | 'running') => void } = {},
  ): Promise<UniversalExecutionResult> {
    const executionId = createExecutionId();
    const csrf = await csrfToken(options.signal);
    let submitted = false;
    const cancelQueuedExecution = () => {
      if (!submitted) return;
      void fetch(`/api/runtime/executions/${encodeURIComponent(executionId)}`, {
        method: 'DELETE', credentials: 'same-origin', cache: 'no-store', keepalive: true,
        headers: { accept: 'application/json', 'x-learnitive-csrf': csrf },
      }).catch(() => undefined);
    };
    options.signal?.addEventListener('abort', cancelQueuedExecution, { once: true });
    try {
      const response = await fetch('/api/runtime/execute', {
        method: 'POST', credentials:'same-origin', cache:'no-store', signal: options.signal,
        headers: {
          accept:'application/json',
          'content-type':'application/json',
          'x-learnitive-csrf': csrf,
          'x-learnitive-execution-id': executionId,
        },
        body: JSON.stringify({ executionId, projectKey, operation, provider, files, entry, command, stdin: String(options.stdin ?? '').slice(0, 1_000_000), artifactPaths:options.artifactPaths || [] }),
      });
      const queued = await response.json().catch(() => ({})) as { error?: string; executionId?: string; stage?: string; status?: string };
      if (!response.ok) {
        const code = SAFE_RUNTIME_ERRORS.has(String(queued.error)) ? String(queued.error) : 'runtime_internal_error';
        throw new ProjectExecutionError(code, {
          executionId: queued.executionId || response.headers.get('x-learnitive-execution-id') || executionId,
          stage: queued.stage,
          statusCode: response.status,
        });
      }
      submitted = true;
      options.onStatus?.('queued');

      const pollStarted = Date.now();
      while (true) {
        options.signal?.throwIfAborted();
        const statusResponse = await fetch(`/api/runtime/executions/${encodeURIComponent(executionId)}`, {
          method: 'GET', credentials: 'same-origin', cache: 'no-store', signal: options.signal,
          headers: { accept: 'application/json' },
        });
        const statusPayload = await statusResponse.json().catch(() => ({})) as {
          error?: string;
          stage?: string;
          status?: string;
          executionId?: string;
          result?: UniversalExecutionResult;
        };
        if (!statusResponse.ok) {
          const code = SAFE_RUNTIME_ERRORS.has(String(statusPayload.error)) ? String(statusPayload.error) : 'runtime_internal_error';
          throw new ProjectExecutionError(code, { executionId, stage: statusPayload.stage, statusCode: statusResponse.status });
        }
        if (statusPayload.result && (statusPayload.status === 'success' || statusPayload.status === 'error')) {
          return { ...statusPayload.result, executionId };
        }
        if (statusPayload.status === 'running') options.onStatus?.('running');
        else if (['queued', 'dispatching', 'waiting'].includes(String(statusPayload.status))) options.onStatus?.('queued');
        else if (statusPayload.status === 'cancelled' || statusPayload.status === 'superseded') {
          throw new ProjectExecutionError('runtime_cancelled', { executionId, stage: statusPayload.stage });
        } else if (statusPayload.status === 'timed_out') {
          throw new ProjectExecutionError('runtime_timeout', { executionId, stage: statusPayload.stage });
        } else if (statusPayload.status === 'error') {
          const code = SAFE_RUNTIME_ERRORS.has(String(statusPayload.error)) ? String(statusPayload.error) : 'runtime_internal_error';
          throw new ProjectExecutionError(code, { executionId, stage: statusPayload.stage });
        }
        const elapsed = Date.now() - pollStarted;
        const delay = elapsed < 3000 ? 200 : elapsed < 15000 ? 400 : 750;
        await new Promise<void>((resolve, reject) => {
          const abort = () => {
            globalThis.clearTimeout(timer);
            reject(options.signal?.reason || new DOMException('Execution cancelled', 'AbortError'));
          };
          const timer = globalThis.setTimeout(() => {
            options.signal?.removeEventListener('abort', abort);
            resolve();
          }, delay);
          options.signal?.addEventListener('abort', abort, { once: true });
        });
      }
    } finally {
      options.signal?.removeEventListener('abort', cancelQueuedExecution);
    }
  }

  async execute(wsName: string, operation: ProjectOperation, options: { activePath?: string; artifactPaths?: string[]; stdin?: string; signal?: AbortSignal; onStatus?: (status: 'queued' | 'running') => void } = {}): Promise<UniversalExecutionResult> {
    const profile = await this.resolveProfile(wsName, options.activePath);
    const resolved = resolveOperation(profile, operation);
    if (!resolved) throw new ProjectExecutionError('runtime_language_unsupported');

    if (operation === 'PREVIEW' && resolved.provider === 'markdown') {
      return { status:'success', operation, provider:resolved.provider, exitCode:0, stdout:'', stderr:'', diagnostics:[], duration:0, artifacts:[], previewTargets:[{kind:'markdown', path:profile.entry}], testResults:[] };
    }
    if (operation === 'PREVIEW' && resolved.provider === 'latex') {
      return { status:'success', operation, provider:resolved.provider, exitCode:0, stdout:'', stderr:'', diagnostics:[], duration:0, artifacts:[], previewTargets:[{kind:'pdf', path:profile.entry}], testResults:[] };
    }

    const files = await this.snapshot(wsName, options.signal);
    return this.executePrepared(
      wsName,
      operation,
      resolved.provider,
      files,
      profile.entry,
      resolved.command,
      { ...options, stdin: String(options.stdin ?? this.getStdin(wsName)).slice(0, 1_000_000) },
    );
  }

  /**
   * Execute one document-owned code snippet through the same authenticated,
   * metered runtime queue used by normal projects. The surrounding document
   * remains a non-runnable `interactive-doc` project; only this bounded virtual
   * file is submitted to the selected trusted runtime profile.
   */
  async executeSnippet(
    projectKey: string,
    input: { projectModeId?: string; language?: string; filename?: string; code: string; stdin?: string },
    options: { signal?: AbortSignal; onStatus?: (status: 'queued' | 'running') => void } = {},
  ): Promise<UniversalExecutionResult> {
    const aliases: Record<string, string> = {
      js:'javascript', javascript:'javascript', ts:'typescript', typescript:'typescript',
      py:'python', python:'python', sh:'shell', bash:'shell', shell:'shell',
      c:'c', cpp:'cpp', 'c++':'cpp', java:'java', go:'go', rust:'rust',
      php:'php', ruby:'ruby', r:'r',
    };
    const requested = String(input.projectModeId || input.language || '').trim().toLowerCase();
    const modeId = aliases[requested] || requested;
    const mode = projectModeById(modeId);
    if (!isRunnableSnippetMode(mode)) {
      throw new ProjectExecutionError('runtime_language_unsupported');
    }
    const profile = canonicalProfileForMode(mode.id);
    const resolved = resolveOperation(profile, 'RUN');
    if (!resolved) throw new ProjectExecutionError('runtime_language_unsupported');
    const filename = String(input.filename || profile.entry || mode.defaultEntryFile).trim();
    if (!filename || filename.includes('..') || filename.startsWith('/') || filename.includes('\\')) {
      throw new Error('Choose a valid snippet filename.');
    }
    const bytes = new TextEncoder().encode(String(input.code ?? ''));
    if (bytes.byteLength > MAX_SINGLE_FILE_BYTES) throw new Error('Code snippet is too large for cloud execution.');
    return this.executePrepared(
      projectKey,
      'RUN',
      resolved.provider,
      [{ path: filename, contentBase64: base64(bytes) }],
      filename,
      resolved.command,
      { signal: options.signal, onStatus: options.onStatus, stdin: String(input.stdin ?? '').slice(0, 1_000_000) },
    );
  }

  async exportMarkdownPdf(
    projectKey: string,
    input: { markdown: string; stylesheet: string; assets?: Array<{ path: string; bytes: Uint8Array }> },
    options: { signal?: AbortSignal; onStatus?: (status: 'queued' | 'running') => void } = {},
  ): Promise<UniversalExecutionResult> {
    const markdownBytes = new TextEncoder().encode(String(input.markdown ?? ''));
    const stylesheetBytes = new TextEncoder().encode(String(input.stylesheet ?? ''));
    if (markdownBytes.byteLength > MAX_SINGLE_FILE_BYTES) throw new Error('Interactive Doc is too large for PDF export.');
    if (stylesheetBytes.byteLength > 1_000_000) throw new Error('Interactive Doc PDF stylesheet is too large.');
    const files: Array<{ path: string; contentBase64: string }> = [
      { path: 'document.md', contentBase64: base64(markdownBytes) },
      { path: 'interactive-doc.css', contentBase64: base64(stylesheetBytes) },
    ];
    let total = markdownBytes.byteLength + stylesheetBytes.byteLength;
    for (const asset of input.assets || []) {
      const path = String(asset.path || '').replaceAll('\\', '/');
      if (!/^assets\/[A-Za-z0-9._/-]+$/u.test(path) || path.includes('..')) throw new Error('Interactive Doc PDF contains an invalid asset path.');
      if (!(asset.bytes instanceof Uint8Array)) throw new Error(`Interactive Doc PDF asset ${path} is invalid.`);
      if (asset.bytes.byteLength > MAX_SINGLE_FILE_BYTES) throw new Error(`Interactive Doc PDF asset ${path} is too large.`);
      total += asset.bytes.byteLength;
      if (total > MAX_SNAPSHOT_BYTES) throw new Error('Interactive Doc PDF assets exceed the export size limit.');
      files.push({ path, contentBase64: base64(asset.bytes) });
    }
    return this.executePrepared(projectKey, 'EXPORT', 'markdown', files, 'document.md', undefined, {
      artifactPaths: ['.learnitive-tmp/document.pdf'],
      signal: options.signal,
      onStatus: options.onStatus,
    });
  }
}
