import {
  BaseService,
  type BaseServiceContext,
  throwAppError,
} from '@learnitive/base-utils';
import { SERVICE_NAME } from '@learnitive/constants';
import {
  createDefaultLearnitiveProjectConfig,
  DEFAULT_LEARNITIVE_MAIN_TEX,
  exportProjectZip,
  exportProjectZipStream,
  importProjectZip,
  importProjectZipBlob,
  isLearnitiveProjectConfig,
  projectFilePath,
  type ProjectFilePath,
  type ProjectStorageProvider,
  type LearnitiveProjectConfig,
} from '@learnitive/project-storage';
import type { WorkspaceInfo } from '@learnitive/types';
import {
  canonicalProfileForMode,
  detectProjectTargets,
  isProjectProfile,
  projectModeById,
  projectModeForProfile,
  type ProjectProfile,
  type ProjectTarget,
} from '@learnitive/project-runtime';
import type { WorkspaceOpsService } from './workspace-ops-service';


const encoder = new TextEncoder();
const defaultMainTexBytes = encoder.encode(DEFAULT_LEARNITIVE_MAIN_TEX);

function bytesEqual(left: Uint8Array | undefined, right: Uint8Array): boolean {
  if (!left || left.byteLength !== right.byteLength) return false;
  for (let index = 0; index < right.byteLength; index += 1) {
    if (left[index] !== right[index]) return false;
  }
  return true;
}

async function isPristineBootstrapProject(
  provider: ProjectStorageProvider,
  signal?: AbortSignal,
): Promise<boolean> {
  const entries = await provider.listEntries(signal);
  const files = entries.filter((entry) => entry.kind === 'file');
  if (files.length !== 1 || String(files[0]?.path) !== 'main.tex') return false;
  const mainTex = await provider.readFile(projectFilePath('main.tex'), signal);
  return bytesEqual(mainTex, defaultMainTexBytes);
}

async function importReplacingPristineBootstrap<T>(
  provider: ProjectStorageProvider,
  importArchive: () => Promise<T>,
  signal?: AbortSignal,
): Promise<T> {
  try {
    return await importArchive();
  } catch (initialError) {
    const collidesWithBootstrapMain =
      initialError instanceof Error &&
      /existing project file: main\.tex/u.test(initialError.message);
    if (!collidesWithBootstrapMain || !(await isPristineBootstrapProject(provider, signal))) {
      throw initialError;
    }
  }

  // Retry only the precise whole-project portability case: the incoming
  // archive contains main.tex and the target contains Learnitive's untouched
  // generated main.tex. Partial archives that do not collide never remove it.
  await provider.deleteFile(projectFilePath('main.tex'), signal);
  try {
    return await importArchive();
  } catch (error) {
    await provider.writeFile(projectFilePath('main.tex'), defaultMainTexBytes, signal);
    throw error;
  }
}

export type ProjectStorageProviderFactory = (
  workspace: WorkspaceInfo,
) => Promise<ProjectStorageProvider>;

export type ProjectStorageMutationEvent =
  | { wsName: string; type: 'write'; path: string }
  | { wsName: string; type: 'create-directory'; path: string }
  | { wsName: string; type: 'delete-file'; path: string }
  | { wsName: string; type: 'delete-directory'; path: string }
  | { wsName: string; type: 'move-file'; from: string; to: string };

type Config = {
  createProvider: ProjectStorageProviderFactory;
  onMutation?: (event: ProjectStorageMutationEvent) => void;
};

/**
 * Owns the storage-neutral Learnitive project boundary. Editor, ZIP, Git, compiler,
 * assets, and AI should consume this service rather than opening OPFS/native
 * handles independently.
 */
class NotifyingProjectStorage implements ProjectStorageProvider {
  constructor(
    private readonly wsName: string,
    private readonly delegate: ProjectStorageProvider,
    private readonly notify: (event: ProjectStorageMutationEvent) => void,
  ) {}

  get capabilities() { return this.delegate.capabilities; }
  listEntries(signal?: AbortSignal) { return this.delegate.listEntries(signal); }
  readFile(path: ProjectFilePath, signal?: AbortSignal) { return this.delegate.readFile(path, signal); }
  async writeFile(path: ProjectFilePath, data: Uint8Array, signal?: AbortSignal) {
    await this.delegate.writeFile(path, data, signal);
    this.notify({ wsName: this.wsName, type: 'write', path: String(path) });
  }
  async createDirectory(path: import('@learnitive/project-storage').ProjectDirectoryPath, signal?: AbortSignal) {
    await this.delegate.createDirectory(path, signal);
    this.notify({ wsName: this.wsName, type: 'create-directory', path: String(path) });
  }
  async deleteFile(path: ProjectFilePath, signal?: AbortSignal) {
    await this.delegate.deleteFile(path, signal);
    this.notify({ wsName: this.wsName, type: 'delete-file', path: String(path) });
  }
  async deleteDirectory(path: import('@learnitive/project-storage').ProjectDirectoryPath, signal?: AbortSignal) {
    await this.delegate.deleteDirectory(path, signal);
    this.notify({ wsName: this.wsName, type: 'delete-directory', path: String(path) });
  }
  async moveFile(from: ProjectFilePath, to: ProjectFilePath, signal?: AbortSignal) {
    await this.delegate.moveFile(from, to, signal);
    this.notify({ wsName: this.wsName, type: 'move-file', from: String(from), to: String(to) });
  }
}

export class ProjectStorageService extends BaseService {
  static deps = ['workspaceOps'] as const;

  private readonly providers = new Map<string, ProjectStorageProvider>();

  constructor(
    context: BaseServiceContext,
    private readonly dependencies: { workspaceOps: WorkspaceOpsService },
    private readonly config: Config,
  ) {
    super(SERVICE_NAME.projectStorageService, context, dependencies);
  }

  async hookMount(): Promise<void> {
    const unsubscribe = this.store.sub(
      this.dependencies.workspaceOps.$workspaceInfoChange,
      () => this.providers.clear(),
    );
    this.addCleanup(unsubscribe);
    this.addCleanup(() => this.providers.clear());
  }

  async getProvider(wsName: string): Promise<ProjectStorageProvider> {
    await this.mountPromise;
    const cached = this.providers.get(wsName);
    if (cached) {
      return cached;
    }

    const workspace = await this.dependencies.workspaceOps.getWorkspaceInfo(wsName);
    if (!workspace) {
      throwAppError(
        'error::workspace:not-found',
        `Workspace not found: ${wsName}`,
        { wsName },
      );
    }

    const rawProvider = await this.config.createProvider(workspace);
    const provider = this.config.onMutation
      ? new NotifyingProjectStorage(wsName, rawProvider, this.config.onMutation)
      : rawProvider;
    this.providers.set(wsName, provider);
    return provider;
  }

  async getProjectConfig(wsName: string): Promise<LearnitiveProjectConfig> {
    await this.mountPromise;
    const metadata = await this.dependencies.workspaceOps.getWorkspaceMetadata(wsName);
    return isLearnitiveProjectConfig(metadata.project)
      ? metadata.project
      : createDefaultLearnitiveProjectConfig();
  }


  async updateProjectConfig(
    wsName: string,
    update: (current: LearnitiveProjectConfig) => LearnitiveProjectConfig,
  ): Promise<LearnitiveProjectConfig> {
    const current = await this.getProjectConfig(wsName);
    const next = update(current);
    if (!isLearnitiveProjectConfig(next)) {
      throw new Error('Invalid Learnitive project configuration update');
    }
    await this.dependencies.workspaceOps.updateWorkspaceMetadata(wsName, (metadata) => ({
      ...metadata,
      project: next,
    }));
    return next;
  }


  async getProjectProfile(wsName: string): Promise<ProjectProfile | undefined> {
    await this.mountPromise;
    const metadata = await this.dependencies.workspaceOps.getWorkspaceMetadata(wsName);
    if (isProjectProfile(metadata.projectProfile)) {
      const mode = projectModeForProfile(metadata.projectProfile);
      return mode && metadata.projectProfile.modeId !== mode.id ? { ...metadata.projectProfile, modeId: mode.id } : metadata.projectProfile;
    }
    const storedMode = projectModeById(typeof metadata.projectModeId === 'string' ? metadata.projectModeId : undefined);
    return storedMode ? canonicalProfileForMode(storedMode.id) : undefined;
  }

  async updateProjectProfile(wsName: string, next: ProjectProfile): Promise<ProjectProfile> {
    if (!isProjectProfile(next)) throw new Error('Invalid Learnitive project profile');
    const mode = projectModeForProfile(next);
    const canonical = mode ? { ...next, modeId: mode.id, entry: next.entry || mode.defaultEntryFile } : next;
    await this.dependencies.workspaceOps.updateWorkspaceMetadata(wsName, (metadata) => {
      const updated: Record<string, unknown> = {
        ...metadata,
        projectProfile: canonical,
        ...(mode ? { projectModeId: mode.id } : {}),
      };
      if (mode?.id === 'latex') {
        if (!isLearnitiveProjectConfig(updated.project)) updated.project = createDefaultLearnitiveProjectConfig();
      } else if (mode) {
        delete updated.project;
      }
      return updated;
    });
    return canonical;
  }

  async detectProjectTargets(
    wsName: string,
    options: { activePath?: string; signal?: AbortSignal } = {},
  ): Promise<ProjectTarget[]> {
    const provider = await this.getProvider(wsName);
    const entries = await provider.listEntries(options.signal);
    const interesting = entries.filter((entry) => entry.kind === 'file' && (
      /(?:^|\/)(?:package\.json|pyproject\.toml|requirements\.txt|Cargo\.toml|go\.mod|pom\.xml|build\.gradle(?:\.kts)?|CMakeLists\.txt|Makefile|vite\.config\.(?:js|ts)|index\.html|README\.md|document\.md|main\.md|main\.(?:py|js|ts|c|cc|cpp|cxx|java|go|rs|php|rb|R|sh)|app\.py|src\/index\.(?:js|ts))$/u.test(String(entry.path))
      || /\.(?:tex|js|jsx|ts|tsx|py|c|cc|cpp|cxx|h|hpp|java|go|rs|php|rb|r|sh|bash|md|markdown)$/iu.test(String(entry.path))
      || String(entry.path) === '.learnitive.yml'
    ) && entry.size <= 256 * 1024);
    const decoder = new TextDecoder('utf-8', { fatal: false });
    const files: Record<string, string> = {};
    for (const entry of interesting.slice(0, 256)) {
      const bytes = await provider.readFile(entry.path, options.signal);
      if (bytes) files[String(entry.path)] = decoder.decode(bytes);
    }
    const selectedProfile = await this.getProjectProfile(wsName);
    const portableConfigText = files['.learnitive.yml'];
    return detectProjectTargets({ files, selectedProfile, portableConfigText, activePath: options.activePath });
  }

  async exportZip(
    wsName: string,
    options: { includeGit?: boolean; signal?: AbortSignal } = {},
  ): Promise<Uint8Array> {
    const provider = await this.getProvider(wsName);
    return exportProjectZip(provider, options);
  }

  async exportZipStream(
    wsName: string,
    options: { includeGit?: boolean; signal?: AbortSignal } = {},
  ): Promise<ReadableStream<Uint8Array>> {
    const provider = await this.getProvider(wsName);
    return exportProjectZipStream(provider, options);
  }

  async importZip(
    wsName: string,
    archive: Uint8Array,
    options: { signal?: AbortSignal } = {},
  ): Promise<ProjectFilePath[]> {
    const provider = await this.getProvider(wsName);
    return importReplacingPristineBootstrap(
      provider,
      () => importProjectZip(archive, provider, options),
      options.signal,
    );
  }

  async importZipBlob(
    wsName: string,
    archive: Blob,
    options: { signal?: AbortSignal } = {},
  ): Promise<ProjectFilePath[]> {
    const provider = await this.getProvider(wsName);
    return importReplacingPristineBootstrap(
      provider,
      () => importProjectZipBlob(archive, provider, options),
      options.signal,
    );
  }
}
