import LightningFS from '@isomorphic-git/lightning-fs';
import git from 'isomorphic-git';
import http from 'isomorphic-git/http/web';
import {
  type ProjectFilePath,
  type ProjectStorageProvider,
} from '@learnitive/project-storage';
import {
  createProjectTextDiff,
  decodeGitText,
  type ProjectGitDiff,
} from './diff';
import type {
  ProjectGitCommit,
  ProjectGitEngine,
  ProjectGitFileChange,
  ProjectGitRemote,
  ProjectGitStatus,
} from './types';
import { mapProjectGitStatus } from './status';
import { applyGitWorkingTree, readProjectWorkingTree } from './working-tree';

const GIT_DIR = '/project';

let bufferReady: Promise<void> | undefined;

async function ensureBuffer(): Promise<void> {
  if (typeof (globalThis as { Buffer?: unknown }).Buffer !== 'undefined') {
    return;
  }
  bufferReady ??= import('buffer').then(({ Buffer }) => {
    (globalThis as { Buffer?: typeof Buffer }).Buffer = Buffer;
  });
  await bufferReady;
}

type LightningPromises = LightningFS['promises'];

type ProjectGitEngineOptions = {
  repositoryId: string;
  storage: ProjectStorageProvider;
  corsProxy?: string;
  defaultAuthor?: { name: string; email: string };
};

function safeRepositoryId(id: string): string {
  const normalized = id
    .replace(/[^a-zA-Z0-9._-]+/g, '-')
    .replace(/^-+|-+$/g, '');
  return normalized || 'project';
}

function authForToken(token?: string) {
  return token
    ? () => ({ username: token, password: 'x-oauth-basic' })
    : undefined;
}

function normalizeCorsProxy(value?: string): string | undefined {
  const trimmed = value?.trim();
  if (!trimmed) return undefined;
  let url: URL;
  try { url = new URL(trimmed); } catch { throw new Error('Git CORS proxy must be a valid URL.'); }
  const loopback = ['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname);
  if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
    throw new Error('Git CORS proxy must use HTTPS (HTTP is allowed only on loopback).');
  }
  if (url.username || url.password || url.search || url.hash) {
    throw new Error('Git CORS proxy URL must not contain credentials, query parameters, or fragments.');
  }
  return url.href.replace(/\/$/u, '');
}

/**
 * Browser Git adapter with a binary-safe project bridge. LightningFS owns the
 * persistent Git object database/index while Learnitive ProjectStorageProvider
 * remains canonical for the user's working files.
 */
export class IsomorphicProjectGitEngine implements ProjectGitEngine {
  readonly available = true;

  private readonly fs: LightningFS;
  private readonly pfs: LightningPromises;
  private readonly storage: ProjectStorageProvider;
  private readonly corsProxy?: string;
  private readonly defaultAuthor: { name: string; email: string };

  constructor(options: ProjectGitEngineOptions) {
    this.fs = new LightningFS(
      `interactive-git-${safeRepositoryId(options.repositoryId)}`,
    );
    this.pfs = this.fs.promises;
    this.storage = options.storage;
    this.corsProxy = normalizeCorsProxy(options.corsProxy);
    this.defaultAuthor = options.defaultAuthor ?? {
      name: 'Learnitive User',
      email: 'user@learnitive.local',
    };
  }

  async init(defaultBranch = 'main'): Promise<void> {
    await ensureBuffer();
    await this.syncProjectToGit();
    if (await this.isRepository()) {
      return;
    }
    await git.init({ fs: this.fs, dir: GIT_DIR, defaultBranch });
  }

  async clone(
    url: string,
    input: { token?: string; corsProxy?: string; singleBranch?: boolean } = {},
  ): Promise<void> {
    await ensureBuffer();
    await this.resetGitFilesystem();
    await this.ensureDirectory(GIT_DIR);
    try {
      await git.clone({
        fs: this.fs,
        http,
        dir: GIT_DIR,
        url,
        corsProxy: normalizeCorsProxy(input.corsProxy) ?? this.corsProxy,
        onAuth: authForToken(input.token),
        singleBranch: input.singleBranch ?? false,
      });
      await this.syncGitToProject();
    } catch (error) {
      await this.resetGitFilesystem().catch(() => undefined);
      throw error;
    }
  }

  async discardLocalRepository(): Promise<void> {
    await ensureBuffer();
    await this.resetGitFilesystem();
  }

  async isRepository(): Promise<boolean> {
    await ensureBuffer();
    try {
      await this.pfs.stat(`${GIT_DIR}/.git`);
      return true;
    } catch {
      return false;
    }
  }

  async status(): Promise<ProjectGitStatus> {
    if (!(await this.isRepository())) {
      return { staged: [], unstaged: [] };
    }
    await this.syncProjectToGit();
    const matrix = await git.statusMatrix({ fs: this.fs, dir: GIT_DIR });
    const staged: ProjectGitFileChange[] = [];
    const unstaged: ProjectGitFileChange[] = [];

    for (const [path, head, workdir, stage] of matrix) {
      if (head === 1 && workdir === 1 && stage === 1) {
        continue;
      }
      const change = mapProjectGitStatus(path, head, workdir, stage);
      if (change.staged) staged.push(change.staged);
      if (change.unstaged) unstaged.push(change.unstaged);
    }

    return {
      branch:
        (await git.currentBranch({ fs: this.fs, dir: GIT_DIR })) ?? undefined,
      staged: staged.sort((a, b) => a.path.localeCompare(b.path)),
      unstaged: unstaged.sort((a, b) => a.path.localeCompare(b.path)),
    };
  }

  async diff(path: ProjectFilePath): Promise<ProjectGitDiff> {
    await this.assertRepository();

    let oldBytes = new Uint8Array();
    try {
      const oid = await git.resolveRef({
        fs: this.fs,
        dir: GIT_DIR,
        ref: 'HEAD',
      });
      const result = await git.readBlob({
        fs: this.fs,
        dir: GIT_DIR,
        oid,
        filepath: path,
      });
      oldBytes = new Uint8Array(result.blob);
    } catch {
      // An unborn HEAD or an untracked path has no previous blob.
    }

    const current = await this.storage.readFile(path);
    const newBytes = current === undefined ? new Uint8Array() : current;
    const oldText = decodeGitText(oldBytes);
    const newText = decodeGitText(newBytes);
    if (oldText === undefined || newText === undefined) {
      return {
        path,
        binary: true,
        oldSize: oldBytes.byteLength,
        newSize: newBytes.byteLength,
      };
    }

    return {
      path,
      binary: false,
      oldSize: oldBytes.byteLength,
      newSize: newBytes.byteLength,
      lines: createProjectTextDiff(oldText, newText),
    };
  }

  async stage(paths: readonly ProjectFilePath[]): Promise<void> {
    await this.assertRepository();
    await this.syncProjectToGit();
    for (const filepath of paths) {
      try {
        await this.pfs.stat(`${GIT_DIR}/${filepath}`);
        await git.add({ fs: this.fs, dir: GIT_DIR, filepath });
      } catch (error) {
        if ((error as { code?: string }).code !== 'ENOENT') {
          throw error;
        }
        await git.remove({ fs: this.fs, dir: GIT_DIR, filepath });
      }
    }
  }

  async unstage(paths: readonly ProjectFilePath[]): Promise<void> {
    await this.assertRepository();
    for (const filepath of paths) {
      await git.resetIndex({ fs: this.fs, dir: GIT_DIR, filepath });
    }
  }

  async commit(input: {
    message: string;
    authorName: string;
    authorEmail: string;
  }): Promise<string> {
    await this.assertRepository();
    return git.commit({
      fs: this.fs,
      dir: GIT_DIR,
      message: input.message,
      author: { name: input.authorName, email: input.authorEmail },
    });
  }

  async log(depth = 50): Promise<readonly ProjectGitCommit[]> {
    if (!(await this.isRepository())) {
      return [];
    }
    try {
      const entries = await git.log({ fs: this.fs, dir: GIT_DIR, depth });
      return entries.map(({ oid, commit }) => ({
        oid,
        message: commit.message,
        authorName: commit.author.name,
        authorEmail: commit.author.email,
        timestamp: commit.author.timestamp,
        parents: commit.parent,
      }));
    } catch {
      return [];
    }
  }

  async listBranches(): Promise<readonly string[]> {
    if (!(await this.isRepository())) {
      return [];
    }
    return git.listBranches({ fs: this.fs, dir: GIT_DIR });
  }

  async createBranch(name: string): Promise<void> {
    await this.assertRepository();
    await git.branch({ fs: this.fs, dir: GIT_DIR, ref: name });
  }

  async deleteBranch(name: string): Promise<void> {
    await this.assertRepository();
    await git.deleteBranch({ fs: this.fs, dir: GIT_DIR, ref: name });
  }

  async checkout(branch: string): Promise<void> {
    await this.assertRepository();
    await this.syncProjectToGit();
    await git.checkout({ fs: this.fs, dir: GIT_DIR, ref: branch });
    await this.syncGitToProject();
  }

  async merge(
    branch: string,
    author: { name: string; email: string } = this.defaultAuthor,
  ): Promise<{ oid?: string; conflicts: readonly string[] }> {
    await this.assertRepository();
    await this.syncProjectToGit();
    const ours = await git.currentBranch({ fs: this.fs, dir: GIT_DIR });
    if (!ours) {
      throw new Error('Cannot merge while Git HEAD is detached');
    }
    try {
      const result = await git.merge({
        fs: this.fs,
        dir: GIT_DIR,
        ours,
        theirs: branch,
        author,
      });
      await this.syncGitToProject();
      return { oid: result.oid, conflicts: [] };
    } catch (error) {
      const candidate = error as { code?: string; data?: { filepaths?: string[] } };
      if (
        candidate.code === 'MergeConflictError' ||
        candidate.code === 'MergeNotSupportedError'
      ) {
        return { conflicts: candidate.data?.filepaths ?? [] };
      }
      throw error;
    }
  }

  async listRemotes(): Promise<readonly ProjectGitRemote[]> {
    if (!(await this.isRepository())) {
      return [];
    }
    return git.listRemotes({ fs: this.fs, dir: GIT_DIR });
  }

  async addRemote(name: string, url: string): Promise<void> {
    await this.assertRepository();
    await git.addRemote({ fs: this.fs, dir: GIT_DIR, remote: name, url });
  }

  async removeRemote(name: string): Promise<void> {
    await this.assertRepository();
    await git.deleteRemote({ fs: this.fs, dir: GIT_DIR, remote: name });
  }

  async fetch(
    input: { remote?: string; token?: string; corsProxy?: string } = {},
  ): Promise<void> {
    await this.assertRepository();
    await git.fetch({
      fs: this.fs,
      http,
      dir: GIT_DIR,
      remote: input.remote ?? 'origin',
      corsProxy: normalizeCorsProxy(input.corsProxy) ?? this.corsProxy,
      onAuth: authForToken(input.token),
    });
  }

  async pull(
    input: {
      remote?: string;
      token?: string;
      corsProxy?: string;
      author?: { name: string; email: string };
    } = {},
  ): Promise<void> {
    await this.assertRepository();
    await this.syncProjectToGit();
    const ref = await git.currentBranch({ fs: this.fs, dir: GIT_DIR });
    if (!ref) {
      throw new Error('Cannot pull while Git HEAD is detached');
    }
    await git.pull({
      fs: this.fs,
      http,
      dir: GIT_DIR,
      remote: input.remote ?? 'origin',
      ref,
      corsProxy: normalizeCorsProxy(input.corsProxy) ?? this.corsProxy,
      onAuth: authForToken(input.token),
      author: input.author ?? this.defaultAuthor,
    });
    await this.syncGitToProject();
  }

  async push(
    input: { remote?: string; token?: string; corsProxy?: string } = {},
  ): Promise<void> {
    await this.assertRepository();
    await this.syncProjectToGit();
    const ref = await git.currentBranch({ fs: this.fs, dir: GIT_DIR });
    if (!ref) {
      throw new Error('Cannot push while Git HEAD is detached');
    }
    await git.push({
      fs: this.fs,
      http,
      dir: GIT_DIR,
      remote: input.remote ?? 'origin',
      ref,
      corsProxy: normalizeCorsProxy(input.corsProxy) ?? this.corsProxy,
      onAuth: authForToken(input.token),
    });
  }

  private async assertRepository(): Promise<void> {
    await ensureBuffer();
    if (!(await this.isRepository())) {
      throw new Error('This project is not a Git repository');
    }
  }

  private async syncProjectToGit(): Promise<void> {
    await this.ensureDirectory(GIT_DIR);
    const existing = await this.pfs.readdir(GIT_DIR).catch(() => [] as string[]);
    for (const entry of existing) {
      if (entry !== '.git') {
        await this.removeRecursive(`${GIT_DIR}/${entry}`);
      }
    }

    const tree = await readProjectWorkingTree(this.storage);
    for (const [path, bytes] of tree) {
      const fullPath = `${GIT_DIR}/${path}`;
      await this.ensureDirectory(fullPath.slice(0, fullPath.lastIndexOf('/')));
      await this.pfs.writeFile(fullPath, bytes);
    }
  }

  private async syncGitToProject(): Promise<void> {
    const tree = new Map<string, Uint8Array>();
    await this.walkGitDirectory(GIT_DIR, '', tree);
    await applyGitWorkingTree(this.storage, tree);
  }

  private async walkGitDirectory(
    directory: string,
    prefix: string,
    result: Map<string, Uint8Array>,
  ): Promise<void> {
    const entries = await this.pfs.readdir(directory).catch(() => [] as string[]);
    for (const entry of entries) {
      if (!prefix && entry === '.git') {
        continue;
      }
      const fullPath = `${directory}/${entry}`;
      const relativePath = prefix ? `${prefix}/${entry}` : entry;
      const stat = await this.pfs.stat(fullPath);
      if (stat.isDirectory()) {
        await this.walkGitDirectory(fullPath, relativePath, result);
      } else {
        const data = await this.pfs.readFile(fullPath);
        const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
        result.set(relativePath, new Uint8Array(bytes));
      }
    }
  }

  private async ensureDirectory(path: string): Promise<void> {
    const parts = path.split('/').filter(Boolean);
    let current = '';
    for (const part of parts) {
      current += `/${part}`;
      try {
        await this.pfs.mkdir(current);
      } catch (error) {
        if ((error as { code?: string }).code !== 'EEXIST') {
          throw error;
        }
      }
    }
  }

  private async removeRecursive(path: string): Promise<void> {
    try {
      const stat = await this.pfs.stat(path);
      if (stat.isDirectory()) {
        for (const entry of await this.pfs.readdir(path)) {
          await this.removeRecursive(`${path}/${entry}`);
        }
        await this.pfs.rmdir(path);
      } else {
        await this.pfs.unlink(path);
      }
    } catch (error) {
      if ((error as { code?: string }).code !== 'ENOENT') {
        throw error;
      }
    }
  }

  private async resetGitFilesystem(): Promise<void> {
    await this.removeRecursive(GIT_DIR);
  }
}
