import { BaseService, type BaseServiceContext } from '@learnitive/base-utils';
import { SERVICE_NAME } from '@learnitive/constants';
import {
  projectModeForProfile,
  resolveProjectRelativePath,
  type ProjectOperation,
  type ProjectProfile,
  type UniversalExecutionResult,
} from '@learnitive/project-runtime';
import { projectFilePath } from '@learnitive/project-storage';
import { atom } from 'jotai';
import type { ProjectCompileService } from './project-compile-service';
import {
  ProjectExecutionError,
  runtimeErrorMessage,
  type ProjectExecutionService,
} from './project-execution-service';
import type { ProjectStorageService } from './project-storage-service';

export type UniversalCompilePhase =
  | 'idle'
  | 'saving'
  | 'queued'
  | 'preparing'
  | 'compiling'
  | 'running'
  | 'success'
  | 'error'
  | 'cancelled';

export type UniversalCompileResultKind = 'console' | 'web-preview' | 'markdown-preview' | 'latex';

export type UniversalCompileState = {
  phase: UniversalCompilePhase;
  wsName?: string;
  activePath?: string;
  profile?: ProjectProfile;
  operation?: ProjectOperation;
  resultKind?: UniversalCompileResultKind;
  result?: UniversalExecutionResult;
  previewHtml?: string;
  error?: string;
  errorCode?: string;
  executionId?: string;
  startedAt?: number;
  duration?: number;
  outputOpen: boolean;
};

export type UniversalCompileRequest = {
  activePath?: string;
  prepare?: () => Promise<void> | void;
};

const EMPTY_STATE: UniversalCompileState = { phase: 'idle', outputOpen: false };
const utf8 = new TextDecoder('utf-8', { fatal: false });

function chooseOperation(profile: ProjectProfile): ProjectOperation {
  if (profile.providers.PREVIEW === 'static-web') return 'PREVIEW';
  if (profile.providers.RUN) return 'RUN';
  if (profile.providers.BUILD) return 'BUILD';
  if (profile.providers.PREVIEW) return 'PREVIEW';
  throw new ProjectExecutionError('runtime_language_unsupported');
}

function escapeScriptClose(text: string): string {
  return text.replace(/<\/script/giu, '<\\/script');
}

const PREVIEW_ASSET_MIME: Readonly<Record<string, string>> = Object.freeze({
  '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
  '.webp': 'image/webp', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
  '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf',
  '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg',
  '.mp4': 'video/mp4', '.webm': 'video/webm',
});

function previewAssetMime(path: string): string | undefined {
  const clean = path.toLowerCase().split(/[?#]/u, 1)[0] ?? '';
  const dot = clean.lastIndexOf('.');
  return dot >= 0 ? PREVIEW_ASSET_MIME[clean.slice(dot)] : undefined;
}

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

async function buildStaticWebPreview(
  storage: ProjectStorageService,
  wsName: string,
  profile: ProjectProfile,
  signal: AbortSignal,
): Promise<string> {
  const provider = await storage.getProvider(wsName);
  const entry = profile.entry || 'index.html';
  const entryBytes = await provider.readFile(projectFilePath(entry), signal);
  if (!entryBytes) throw new ProjectExecutionError('runtime_language_unsupported');
  let html = utf8.decode(entryBytes);

  const readAsset = async (basePath: string, reference: string): Promise<{ path: string; bytes: Uint8Array } | undefined> => {
    const resolved = resolveProjectRelativePath(basePath, reference);
    if (!resolved) return undefined;
    const bytes = await provider.readFile(projectFilePath(resolved), signal);
    return bytes ? { path: resolved, bytes } : undefined;
  };

  const readText = async (basePath: string, reference: string): Promise<{ path: string; text: string } | undefined> => {
    const asset = await readAsset(basePath, reference);
    return asset ? { path: asset.path, text: utf8.decode(asset.bytes) } : undefined;
  };

  const dataUri = async (basePath: string, reference: string): Promise<string | undefined> => {
    const asset = await readAsset(basePath, reference);
    if (!asset) return undefined;
    const mime = previewAssetMime(asset.path);
    if (!mime) return undefined;
    return `data:${mime};base64,${bytesToBase64(asset.bytes)}`;
  };

  const inlineCssAssets = async (css: string, cssPath: string): Promise<string> => {
    const matches = [...css.matchAll(/url\(\s*(['"]?)([^'"\)]+)\1\s*\)/giu)];
    let transformed = css;
    for (const match of matches) {
      const reference = (match[2] ?? '').trim();
      if (!reference || /^(?:data:|blob:|#)/iu.test(reference)) continue;
      const uri = await dataUri(cssPath, reference);
      if (uri) transformed = transformed.replace(match[0], () => `url("${uri}")`);
    }
    return transformed;
  };

  const linkMatches = [...html.matchAll(/<link\b([^>]*?)\bhref=(['"])([^'"]+)\2([^>]*)>/giu)];
  for (const match of linkMatches) {
    if (!/\brel=(['"])stylesheet\1/iu.test(`${match[1] ?? ''} ${match[4] ?? ''}`)) continue;
    const css = await readText(entry, match[3] ?? '');
    if (!css) continue;
    const transformedCss = await inlineCssAssets(css.text, css.path);
    html = html.replace(match[0], () => `<style data-learnitive-preview>${transformedCss.replace(/<\/style/giu, '<\\/style')}</style>`);
  }

  const scriptMatches = [...html.matchAll(/<script\b([^>]*?)\bsrc=(['"])([^'"]+)\2([^>]*)>\s*<\/script>/giu)];
  for (const match of scriptMatches) {
    const script = await readText(entry, match[3] ?? '');
    if (!script) continue;
    html = html.replace(match[0], () => `<script data-learnitive-preview>${escapeScriptClose(script.text)}</script>`);
  }

  const assetMatches = [...html.matchAll(/\b(src|poster)=(['"])([^'"]+)\2/giu)];
  for (const match of assetMatches) {
    const reference = match[3] ?? '';
    const uri = await dataUri(entry, reference);
    if (uri) html = html.replace(match[0], () => `${match[1]}=${match[2]}${uri}${match[2]}`);
  }

  const bridge = `<script>(function(){const send=(level,args)=>parent.postMessage({source:'learnitive-preview',level,text:args.map(v=>{try{return typeof v==='string'?v:JSON.stringify(v)}catch{return String(v)}}).join(' ')},'*');for(const level of ['log','info','warn','error']){const original=console[level].bind(console);console[level]=(...args)=>{send(level,args);original(...args)}};addEventListener('error',e=>send('error',[e.message||'Preview error']));addEventListener('unhandledrejection',e=>send('error',[e.reason instanceof Error?e.reason.message:String(e.reason)]));})();</script>`;
  const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline'; img-src data: blob:; font-src data: blob:; media-src data: blob:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">`;
  if (/<head[\s>]/iu.test(html)) {
    html = html.replace(/<head([^>]*)>/iu, `<head$1>${csp}${bridge}`);
  } else {
    html = `${csp}${bridge}${html}`;
  }
  return html;
}

type InlinePreviewArtifact = { path: string; mediaType?: string; contentBase64: string };

function isInlinePreviewArtifact(value: unknown): value is InlinePreviewArtifact {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
  const candidate = value as Record<string, unknown>;
  return typeof candidate.path === 'string' && typeof candidate.contentBase64 === 'string' &&
    (candidate.mediaType === undefined || typeof candidate.mediaType === 'string');
}

function buildArtifactWebPreview(result: UniversalExecutionResult, outputDirectory = 'dist'): string | undefined {
  const normalizedOutput = outputDirectory.replace(/^\/+|\/+$/gu, '') || 'dist';
  const artifacts = new Map<string, InlinePreviewArtifact>((Array.isArray(result.artifacts) ? result.artifacts : [])
    .filter(isInlinePreviewArtifact)
    .map((item) => [item.path.replaceAll('\\','/'), item]));
  const entryPath = `${normalizedOutput}/index.html`;
  const entry = artifacts.get(entryPath);
  if (!entry?.contentBase64) return undefined;
  const decodeBytes = (value: string): Uint8Array => {
    const binary = atob(value); const bytes = new Uint8Array(binary.length);
    for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
    return bytes;
  };
  const decodeText = (value: string): string => utf8.decode(decodeBytes(value));
  const resolve = (basePath: string, reference: string): string | undefined => {
    const raw = String(reference || '').trim().split(/[?#]/u, 1)[0]?.replaceAll('\\','/');
    if (!raw || /^(?:data:|blob:|https?:|\/\/|#)/iu.test(raw)) return undefined;
    if (raw.startsWith('/')) return `${normalizedOutput}/${raw.replace(/^\/+/, '')}`;
    return resolveProjectRelativePath(basePath, raw);
  };
  const assetDataUri = (path: string): string | undefined => {
    const artifact = artifacts.get(path); if (!artifact?.contentBase64) return undefined;
    const mime = artifact.mediaType || previewAssetMime(path) || 'application/octet-stream';
    return `data:${mime};base64,${artifact.contentBase64}`;
  };
  const inlineCss = (css: string, cssPath: string): string => css.replace(/url\(\s*(['"]?)([^'"\)]+)\1\s*\)/giu, (full, _quote, reference) => {
    const resolved = resolve(cssPath, reference); const uri = resolved ? assetDataUri(resolved) : undefined;
    return uri ? `url("${uri}")` : full;
  });
  const rewriteJsModule = (path: string, stack = new Set<string>()): string | undefined => {
    const artifact = artifacts.get(path); if (!artifact?.contentBase64 || stack.has(path)) return undefined;
    const nextStack = new Set(stack); nextStack.add(path);
    let code = decodeText(artifact.contentBase64);
    const replacementUri = (reference: string): string | undefined => {
      const resolved = resolve(path, reference);
      if (!resolved) return undefined;
      const nested = rewriteJsModule(resolved, nextStack);
      if (!nested) return undefined;
      return `data:text/javascript;base64,${bytesToBase64(new TextEncoder().encode(nested))}`;
    };
    code = code.replace(/(\bfrom\s*)(['"])([^'"]+)\2/gu, (full, prefix, quote, reference) => {
      const uri = replacementUri(reference); return uri ? `${prefix}${quote}${uri}${quote}` : full;
    });
    code = code.replace(/(\bimport\s*\(\s*)(['"])([^'"]+)\2(\s*\))/gu, (full, prefix, quote, reference, suffix) => {
      const uri = replacementUri(reference); return uri ? `${prefix}${quote}${uri}${quote}${suffix}` : full;
    });
    code = code.replace(/(\bimport\s*)(['"])([^'"]+)\2/gu, (full, prefix, quote, reference) => {
      const uri = replacementUri(reference); return uri ? `${prefix}${quote}${uri}${quote}` : full;
    });
    return code;
  };
  let html = decodeText(entry.contentBase64);
  for (const match of [...html.matchAll(/<link\b([^>]*?)\bhref=(['"])([^'"]+)\2([^>]*)>/giu)]) {
    if (!/\brel=(['"])stylesheet\1/iu.test(`${match[1] ?? ''} ${match[4] ?? ''}`)) continue;
    const resolved = resolve(entryPath, match[3] ?? ''); const artifact = resolved ? artifacts.get(resolved) : undefined;
    if (!resolved || !artifact?.contentBase64) continue;
    html = html.replace(match[0], () => `<style data-learnitive-preview>${inlineCss(decodeText(artifact.contentBase64), resolved).replace(/<\/style/giu,'<\\/style')}</style>`);
  }
  for (const match of [...html.matchAll(/<script\b([^>]*?)\bsrc=(['"])([^'"]+)\2([^>]*)>\s*<\/script>/giu)]) {
    const resolved = resolve(entryPath, match[3] ?? ''); const code = resolved ? rewriteJsModule(resolved) : undefined;
    if (code === undefined) continue;
    const attrs = `${match[1] ?? ''} ${match[4] ?? ''}`;
    const type = /\btype=(['"])module\1/iu.test(attrs) ? ' type="module"' : '';
    html = html.replace(match[0], () => `<script${type} data-learnitive-preview>${escapeScriptClose(code)}</script>`);
  }
  for (const match of [...html.matchAll(/\b(src|poster)=(['"])([^'"]+)\2/giu)]) {
    const resolved = resolve(entryPath, match[3] ?? ''); const uri = resolved ? assetDataUri(resolved) : undefined;
    if (uri) html = html.replace(match[0], () => `${match[1]}=${match[2]}${uri}${match[2]}`);
  }
  const bridge = `<script>(function(){const send=(level,args)=>parent.postMessage({source:'learnitive-preview',level,text:args.map(v=>{try{return typeof v==='string'?v:JSON.stringify(v)}catch{return String(v)}}).join(' ')},'*');for(const level of ['log','info','warn','error']){const original=console[level].bind(console);console[level]=(...args)=>{send(level,args);original(...args)}};addEventListener('error',e=>send('error',[e.message||'Preview error']));addEventListener('unhandledrejection',e=>send('error',[e.reason instanceof Error?e.reason.message:String(e.reason)]));})();</script>`;
  const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' data: blob:; style-src 'unsafe-inline'; img-src data: blob:; font-src data: blob:; media-src data: blob:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">`;
  return /<head[\s>]/iu.test(html) ? html.replace(/<head([^>]*)>/iu, `<head$1>${csp}${bridge}`) : `${csp}${bridge}${html}`;
}

function buildMarkdownArtifactPreview(result: UniversalExecutionResult): string | undefined {
  const artifact = (Array.isArray(result.artifacts) ? result.artifacts : []).find((item) =>
    item.path.replaceAll('\\', '/').endsWith('.learnitive-tmp/document.html') && typeof item.contentBase64 === 'string',
  );
  if (!artifact?.contentBase64) return undefined;
  try {
    const binary = atob(artifact.contentBase64);
    const bytes = new Uint8Array(binary.length);
    for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
    return utf8.decode(bytes);
  } catch {
    return undefined;
  }
}

export class ProjectCompileOrchestratorService extends BaseService {
  static deps = ['projectExecution', 'projectCompile', 'projectStorage'] as const;

  readonly $state = atom<UniversalCompileState>(EMPTY_STATE);
  private running?: AbortController;

  constructor(
    context: BaseServiceContext,
    private readonly dependencies: {
      projectExecution: ProjectExecutionService;
      projectCompile: ProjectCompileService;
      projectStorage: ProjectStorageService;
    },
  ) {
    super(SERVICE_NAME.projectCompileOrchestratorService, context, dependencies);
  }

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

  setOutputVisibility(wsName: string, profile: ProjectProfile, outputOpen: boolean): void {
    this.store.set(this.$state, { ...this.store.get(this.$state), wsName, profile, outputOpen });
  }

  openOutput(wsName?: string, profile?: ProjectProfile): void {
    this.store.set(this.$state, { ...this.store.get(this.$state), ...(wsName ? { wsName } : {}), ...(profile ? { profile } : {}), outputOpen: true });
  }

  closeOutput(): void {
    this.store.set(this.$state, { ...this.store.get(this.$state), outputOpen: false });
  }

  getState(): UniversalCompileState {
    return this.store.get(this.$state);
  }

  /**
   * Publish a web project that was already verified by another trusted flow
   * (for example the AI Agent's persistent sandbox verification) into the
   * normal Compile/Preview surface without running a second build. Static web
   * projects still materialize the canonical local preview HTML so the iframe
   * refreshes immediately; dynamic web projects let ProjectExecutionPane
   * attach to the already-running/reusable dev process.
   */
  async presentVerifiedWebOutput(wsName: string, suppliedProfile?: ProjectProfile): Promise<boolean> {
    await this.mountPromise;
    const profile = suppliedProfile ?? await this.dependencies.projectExecution.resolveProfile(wsName);
    const mode = projectModeForProfile(profile);
    if (mode?.projectType !== 'web') return false;
    const startedAt = performance.now();
    const previewHtml = profile.providers.PREVIEW === 'static-web'
      ? await buildStaticWebPreview(this.dependencies.projectStorage, wsName, profile, new AbortController().signal)
      : undefined;
    this.store.set(this.$state, {
      phase: 'success',
      wsName,
      profile,
      operation: 'PREVIEW',
      resultKind: 'web-preview',
      ...(previewHtml ? { previewHtml } : {}),
      outputOpen: true,
      startedAt,
      duration: performance.now() - startedAt,
    });
    return true;
  }

  reset(): void {
    this.cancel();
    this.store.set(this.$state, EMPTY_STATE);
  }

  cancel(): void {
    const running = this.running;
    if (!running) return;
    running.abort(new DOMException('Compile cancelled', 'AbortError'));
    this.dependencies.projectCompile.cancel();
    this.running = undefined;
    const current = this.store.get(this.$state);
    this.store.set(this.$state, { ...current, phase: 'cancelled', outputOpen: true });
  }

  async compile(wsName: string, request: UniversalCompileRequest = {}): Promise<void> {
    await this.mountPromise;
    this.cancel();
    const controller = new AbortController();
    this.running = controller;
    const startedAt = performance.now();
    const activePath = request.activePath;

    try {
      this.store.set(this.$state, {
        phase: request.prepare ? 'saving' : 'preparing',
        wsName,
        activePath,
        outputOpen: true,
        startedAt,
      });
      await request.prepare?.();
      controller.signal.throwIfAborted();

      this.store.set(this.$state, {
        ...this.store.get(this.$state),
        phase: 'preparing',
      });
      const profile = await this.dependencies.projectExecution.resolveProfile(wsName, activePath);
      const operation = chooseOperation(profile);
      controller.signal.throwIfAborted();

      if (profile.providers.BUILD === 'latex' || profile.providers.PREVIEW === 'latex' || profile.projectType === 'latex' || /(?:^|\/)\w+\.tex$/iu.test(activePath || profile.entry || '')) {
        this.store.set(this.$state, {
          phase: 'compiling', wsName, activePath, profile, operation: 'BUILD',
          resultKind: 'latex', outputOpen: true, startedAt,
        });
        const result = await this.dependencies.projectCompile.compile(wsName, controller.signal);
        controller.signal.throwIfAborted();
        this.store.set(this.$state, {
          phase: result.success ? 'success' : 'error', wsName, activePath, profile,
          operation: 'BUILD', resultKind: 'latex', outputOpen: true, startedAt,
          duration: performance.now() - startedAt,
          ...(result.success ? {} : { error: result.diagnostics.find((item) => item.severity === 'error')?.message || 'LaTeX compilation failed' }),
        });
        return;
      }

      if (operation === 'PREVIEW' && profile.providers.PREVIEW === 'static-web') {
        this.store.set(this.$state, {
          phase: 'compiling', wsName, activePath, profile, operation,
          resultKind: 'web-preview', outputOpen: true, startedAt,
        });
        const previewHtml = await buildStaticWebPreview(
          this.dependencies.projectStorage, wsName, profile, controller.signal,
        );
        controller.signal.throwIfAborted();
        this.store.set(this.$state, {
          phase: 'success', wsName, activePath, profile, operation,
          resultKind: 'web-preview', previewHtml, outputOpen: true, startedAt,
          duration: performance.now() - startedAt,
        });
        return;
      }

      this.store.set(this.$state, {
        phase: operation === 'BUILD' ? 'compiling' : 'running', wsName, activePath,
        profile, operation, resultKind: 'console', outputOpen: true, startedAt,
      });
      const result = await this.dependencies.projectExecution.execute(wsName, operation, {
        activePath,
        signal: controller.signal,
        onStatus: (status) => {
          if (controller.signal.aborted) return;
          const current = this.store.get(this.$state);
          this.store.set(this.$state, {
            ...current,
            phase: status === 'queued' ? 'queued' : operation === 'BUILD' ? 'compiling' : 'running',
            outputOpen: true,
          });
        },
      });
      controller.signal.throwIfAborted();
      const mode = projectModeForProfile(profile);
      const isWeb = mode?.projectType === 'web' || result.provider === 'vite';
      const isMarkdown = mode?.id === 'markdown' || profile.projectType === 'markdown' || result.provider === 'markdown';
      const previewHtml = isWeb && result.status === 'success' && result.provider === 'vite'
        ? buildArtifactWebPreview(result, mode?.buildOutput || 'dist')
        : isMarkdown && result.status === 'success'
          ? buildMarkdownArtifactPreview(result)
          : undefined;
      const missingPreview = (isWeb && result.status === 'success' && result.provider === 'vite' && !previewHtml)
        || (isMarkdown && result.status === 'success' && !previewHtml);
      this.store.set(this.$state, {
        phase: result.status === 'success' && !missingPreview ? 'success' : 'error',
        wsName, activePath, profile, operation, resultKind: isWeb ? 'web-preview' : isMarkdown ? 'markdown-preview' : 'console', result,
        ...(previewHtml ? { previewHtml } : {}),
        executionId: result.executionId, outputOpen: true, startedAt,
        duration: result.duration || performance.now() - startedAt,
        ...(missingPreview ? { error: runtimeErrorMessage('runtime_preview_missing'), errorCode: 'runtime_preview_missing' }
          : result.status === 'success' ? {} : {
            error: result.stderr || result.diagnostics[0]?.message || runtimeErrorMessage('runtime_execution_failed'),
            errorCode: 'runtime_execution_failed',
          }),
      });
    } catch (error) {
      const cancelled = controller.signal.aborted || (error instanceof DOMException && error.name === 'AbortError');
      const current = this.store.get(this.$state);
      const executionError = error instanceof ProjectExecutionError ? error : undefined;
      this.store.set(this.$state, {
        ...current,
        phase: cancelled ? 'cancelled' : 'error',
        outputOpen: true,
        duration: performance.now() - startedAt,
        ...(cancelled ? { error: undefined } : {
          error: executionError?.message || (error instanceof Error ? error.message : runtimeErrorMessage('runtime_internal_error')),
          errorCode: executionError?.code || 'runtime_internal_error',
          executionId: executionError?.executionId,
        }),
      });
      if (!cancelled) throw error;
    } finally {
      if (this.running === controller) this.running = undefined;
    }
  }
}
