import { BaseService, type BaseServiceContext } from '@learnitive/base-utils';
import { SERVICE_NAME } from '@learnitive/constants';
import {
  IsomorphicProjectGitEngine,
  type ProjectGitCommit,
  type ProjectGitDiff,
  type ProjectGitEngine,
  type ProjectGitRemote,
  type ProjectGitStatus,
} from '@learnitive/project-git';
import type { ProjectFilePath } from '@learnitive/project-storage';
import type { EditorService } from './editor-service';
import type { FileSystemService } from './file-system-service';
import type { LatexSourceService } from './latex-source-service';
import type { ProjectStorageService } from './project-storage-service';
import type { ProjectCollaborationService } from './project-collaboration-service';
import type { WorkspaceOpsService } from './workspace-ops-service';

/**
 * Coordinates browser Git with Learnitive's durable project/source services.
 * Git never edits a second application document model: checkout/pull/clone
 * write through the binary-safe project provider and then refresh open source
 * documents plus the workspace tree.
 */
export class ProjectGitService extends BaseService {
  static deps = [
    'editorService',
    'fileSystem',
    'latexSource',
    'projectStorage',
    'projectCollaboration',
    'workspaceOps',
  ] as const;

  private readonly engines = new Map<string, Promise<ProjectGitEngine>>();

  constructor(
    context: BaseServiceContext,
    private readonly dependencies: {
      editorService: EditorService;
      fileSystem: FileSystemService;
      latexSource: LatexSourceService;
      projectStorage: ProjectStorageService;
      projectCollaboration: ProjectCollaborationService;
      workspaceOps: WorkspaceOpsService;
    },
  ) {
    super(SERVICE_NAME.projectGitService, context, dependencies);
  }

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

  async isRepository(wsName: string): Promise<boolean> {
    return (await this.getEngine(wsName)).isRepository();
  }

  async discardLocalRepository(wsName: string): Promise<void> {
    const engine = await this.getEngine(wsName);
    await engine.discardLocalRepository();
    this.engines.delete(wsName);
  }

  async init(wsName: string, defaultBranch = 'main'): Promise<void> {
    await this.flushCollaborationAndSources(wsName);
    await (await this.getEngine(wsName)).init(defaultBranch);
  }

  async status(wsName: string): Promise<ProjectGitStatus> {
    await this.flushCollaborationAndSources(wsName);
    return (await this.getEngine(wsName)).status();
  }

  async diff(wsName: string, path: ProjectFilePath): Promise<ProjectGitDiff> {
    await this.flushCollaborationAndSources(wsName);
    return (await this.getEngine(wsName)).diff(path);
  }

  async stage(wsName: string, paths: readonly ProjectFilePath[]): Promise<void> {
    await this.flushCollaborationAndSources(wsName);
    await (await this.getEngine(wsName)).stage(paths);
  }

  async unstage(
    wsName: string,
    paths: readonly ProjectFilePath[],
  ): Promise<void> {
    await (await this.getEngine(wsName)).unstage(paths);
  }

  async commit(
    wsName: string,
    input: { message: string; authorName: string; authorEmail: string },
  ): Promise<string> {
    await this.flushCollaborationAndSources(wsName);
    return (await this.getEngine(wsName)).commit(input);
  }

  async log(wsName: string, depth = 50): Promise<readonly ProjectGitCommit[]> {
    return (await this.getEngine(wsName)).log(depth);
  }

  async listBranches(wsName: string): Promise<readonly string[]> {
    return (await this.getEngine(wsName)).listBranches();
  }

  async createBranch(wsName: string, name: string): Promise<void> {
    await (await this.getEngine(wsName)).createBranch(name);
  }

  async deleteBranch(wsName: string, name: string): Promise<void> {
    await (await this.getEngine(wsName)).deleteBranch(name);
  }

  async checkout(wsName: string, branch: string): Promise<void> {
    this.dependencies.projectCollaboration.assertDisruptiveGitAllowed(wsName, 'switch branches');
    await this.assertClean(wsName, 'switch branches');
    await (await this.getEngine(wsName)).checkout(branch);
    await this.afterWorkingTreeChange(wsName);
  }

  async merge(
    wsName: string,
    branch: string,
    author?: { name: string; email: string },
  ): Promise<{ oid?: string; conflicts: readonly string[] }> {
    this.dependencies.projectCollaboration.assertDisruptiveGitAllowed(wsName, 'merge branches');
    await this.assertClean(wsName, 'merge branches');
    const result = await (await this.getEngine(wsName)).merge(branch, author);
    if (result.conflicts.length === 0) {
      await this.afterWorkingTreeChange(wsName);
    }
    return result;
  }

  async listRemotes(wsName: string): Promise<readonly ProjectGitRemote[]> {
    return (await this.getEngine(wsName)).listRemotes();
  }

  async addRemote(wsName: string, name: string, url: string): Promise<void> {
    await (await this.getEngine(wsName)).addRemote(name, url);
  }

  async removeRemote(wsName: string, name: string): Promise<void> {
    await (await this.getEngine(wsName)).removeRemote(name);
  }

  async fetch(
    wsName: string,
    input: { remote?: string; token?: string; corsProxy?: string } = {},
  ): Promise<void> {
    await (await this.getEngine(wsName)).fetch(input);
  }

  async pull(
    wsName: string,
    input: {
      remote?: string;
      token?: string;
      corsProxy?: string;
      author?: { name: string; email: string };
    } = {},
  ): Promise<void> {
    this.dependencies.projectCollaboration.assertDisruptiveGitAllowed(wsName, 'pull remote changes');
    await this.assertClean(wsName, 'pull remote changes');
    await (await this.getEngine(wsName)).pull(input);
    await this.afterWorkingTreeChange(wsName);
  }

  async push(
    wsName: string,
    input: { remote?: string; token?: string; corsProxy?: string } = {},
  ): Promise<void> {
    await this.flushCollaborationAndSources(wsName);
    await (await this.getEngine(wsName)).push(input);
  }

  /** Clone is intended for a newly created/empty workspace. */
  async clone(
    wsName: string,
    url: string,
    input: { token?: string; corsProxy?: string; singleBranch?: boolean } = {},
  ): Promise<void> {
    this.dependencies.projectCollaboration.assertDisruptiveGitAllowed(wsName, 'clone a repository');
    await this.flushCollaborationAndSources(wsName);
    const storage = await this.dependencies.projectStorage.getProvider(wsName);
    const userFiles = (await storage.listEntries()).filter(
      (entry) => entry.kind === 'file' && entry.path !== 'learnitive.json',
    );
    if (userFiles.length > 0) {
      throw new Error('Clone requires an empty Learnitive workspace');
    }
    await (await this.getEngine(wsName)).clone(url, input);
    await this.afterWorkingTreeChange(wsName);
  }

  private async getEngine(wsName: string): Promise<ProjectGitEngine> {
    const existing = this.engines.get(wsName);
    if (existing) {
      return existing;
    }

    let pending: Promise<ProjectGitEngine>;
    pending = this.createEngine(wsName).catch((error) => {
      if (this.engines.get(wsName) === pending) {
        this.engines.delete(wsName);
      }
      throw error;
    });
    this.engines.set(wsName, pending);
    return pending;
  }

  private async createEngine(wsName: string): Promise<ProjectGitEngine> {
    const [storage, repositoryId] = await Promise.all([
      this.dependencies.projectStorage.getProvider(wsName),
      this.getRepositoryId(wsName),
    ]);
    return new IsomorphicProjectGitEngine({ repositoryId, storage });
  }

  private async getRepositoryId(wsName: string): Promise<string> {
    const metadata = await this.dependencies.workspaceOps.getWorkspaceMetadata(
      wsName,
    );
    const existing = metadata.learnitiveGitRepositoryId;
    if (typeof existing === 'string' && /^[a-f0-9]{32}$/.test(existing)) {
      return existing;
    }

    const bytes = new Uint8Array(16);
    globalThis.crypto.getRandomValues(bytes);
    const repositoryId = [...bytes]
      .map((byte) => byte.toString(16).padStart(2, '0'))
      .join('');
    await this.dependencies.workspaceOps.updateWorkspaceMetadata(
      wsName,
      (current) => ({ ...current, learnitiveGitRepositoryId: repositoryId }),
    );
    return repositoryId;
  }

  private async flushCollaborationAndSources(wsName: string): Promise<void> {
    await this.dependencies.projectCollaboration.flushToStorage(wsName);
    await this.flushSourcesOrThrow(wsName);
  }

  private async flushSourcesOrThrow(wsName: string): Promise<void> {
    await this.dependencies.latexSource.flushWorkspace(wsName);
    if (this.dependencies.latexSource.hasPendingOrFailedSaveInWorkspace(wsName)) {
      throw new Error(
        'Git operation blocked because a LaTeX source file could not be saved safely',
      );
    }
  }

  private async assertClean(wsName: string, operation: string): Promise<void> {
    await this.flushSourcesOrThrow(wsName);
    const status = await (await this.getEngine(wsName)).status();
    if (status.staged.length > 0 || status.unstaged.length > 0) {
      throw new Error(
        `Commit or revert project changes before you ${operation}`,
      );
    }
  }

  private async afterWorkingTreeChange(wsName: string): Promise<void> {
    await this.dependencies.latexSource.refreshLoadedDocuments(wsName);
    this.dependencies.fileSystem.refreshFileTree();
    this.dependencies.editorService.reloadWorkspace(wsName);
  }
}
