import type { Services } from '@learnitive/context';
import { normalizeProjectSlug, projectFilePath, type ProjectEntry } from '@learnitive/project-storage';
import { WsPath } from '@learnitive/ws-path';
import {
  deleteLinuxTerminalWorkspaceEntry,
  fetchLinuxTerminalFile,
  getLinuxTerminalWorkspaceManifest,
  uploadLinuxTerminalFileChunk,
  type LinuxTerminalSession,
  type TerminalWorkspaceManifestFile,
} from './saas-api';

const CHUNK_BYTES = 4 * 1024 * 1024;
const EXCLUDED_SEGMENTS = new Set(['node_modules', '.git', '.learnitive', '.cache', '.vite', '.venv', 'venv', '__pycache__', 'coverage', '.next', '.nuxt']);
const EXCLUDED_TOP_LEVEL = new Set(['dist', 'build', 'target']);

function excluded(path: string): boolean {
  const value = path.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/$/u, '');
  const segments = value.split('/').filter(Boolean);
  if (segments.some((segment) => EXCLUDED_SEGMENTS.has(segment))) return true;
  return segments.length > 0 && EXCLUDED_TOP_LEVEL.has(segments[0]!);
}

async function sha256(bytes: Uint8Array): Promise<string> {
  const digest = await crypto.subtle.digest('SHA-256', bytes);
  return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, '0')).join('');
}

type HashEntry = { sha256: string; size: number };
type HashManifest = Record<string, HashEntry>;
type SyncBaseline = { wsName: string; slug: string; allocationKey: string; files: HashManifest };

function allocationKey(session: LinuxTerminalSession): string {
  return String(session.sandboxExpiresAt || session.expiresAt || 'unknown');
}
function storageKey(session: LinuxTerminalSession, wsName: string, slug: string): string {
  return `learnitive:terminal-sync:v1:${allocationKey(session)}:${wsName}:${slug}`;
}
function readBaseline(session: LinuxTerminalSession, wsName: string, slug: string): SyncBaseline | undefined {
  try {
    const parsed = JSON.parse(localStorage.getItem(storageKey(session, wsName, slug)) || 'null') as SyncBaseline | null;
    if (!parsed || parsed.wsName !== wsName || parsed.slug !== slug || parsed.allocationKey !== allocationKey(session) || typeof parsed.files !== 'object') return undefined;
    return parsed;
  } catch {
    return undefined;
  }
}
function writeBaseline(session: LinuxTerminalSession, wsName: string, slug: string, files: HashManifest): void {
  try {
    localStorage.setItem(storageKey(session, wsName, slug), JSON.stringify({ wsName, slug, allocationKey: allocationKey(session), files } satisfies SyncBaseline));
  } catch {
    // Sync remains functional without persistence; the next load simply re-baselines.
  }
}

async function localManifest(core: Pick<Services, 'projectStorage'>, wsName: string): Promise<{ files: HashManifest; entries: Map<string, Extract<ProjectEntry, { kind: 'file' }>> }> {
  const provider = await core.projectStorage.getProvider(wsName);
  const entries = (await provider.listEntries()).filter((entry): entry is Extract<ProjectEntry, { kind: 'file' }> => entry.kind === 'file' && !excluded(String(entry.path)));
  const files: HashManifest = {};
  const map = new Map<string, Extract<ProjectEntry, { kind: 'file' }>>();
  for (const entry of entries) {
    const path = String(entry.path);
    const bytes = await provider.readFile(entry.path);
    if (!bytes) continue;
    files[path] = { sha256: await sha256(bytes), size: bytes.byteLength };
    map.set(path, entry);
  }
  return { files, entries: map };
}

function remoteManifestMap(files: TerminalWorkspaceManifestFile[], slug: string): HashManifest {
  const prefix = `projects/${slug}/`;
  const out: HashManifest = {};
  for (const file of files) {
    if (!file.path.startsWith(prefix)) continue;
    const relative = file.path.slice(prefix.length);
    if (!relative || excluded(relative)) continue;
    out[relative] = { sha256: file.sha256, size: file.size };
  }
  return out;
}

async function uploadBytes(sessionId: string, remotePath: string, bytes: Uint8Array, onProgress?: (uploaded: number, total: number) => void): Promise<void> {
  if (!bytes.byteLength) {
    await uploadLinuxTerminalFileChunk(sessionId, remotePath, 0, true, bytes);
    onProgress?.(0, 0);
    return;
  }
  let offset = 0;
  while (offset < bytes.byteLength) {
    const end = Math.min(bytes.byteLength, offset + CHUNK_BYTES);
    const chunk = bytes.subarray(offset, end);
    await uploadLinuxTerminalFileChunk(sessionId, remotePath, offset, end === bytes.byteLength, chunk);
    offset = end;
    onProgress?.(offset, bytes.byteLength);
  }
}


export type TerminalProjectLink = { wsName: string; slug: string; sourceType: string; loadedAt: number };
function linksStorageKey(session: LinuxTerminalSession): string { return `learnitive:terminal-links:v1:${allocationKey(session)}`; }
export function listTerminalProjectLinks(session: LinuxTerminalSession): TerminalProjectLink[] {
  try {
    const parsed = JSON.parse(localStorage.getItem(linksStorageKey(session)) || '[]') as TerminalProjectLink[];
    return Array.isArray(parsed) ? parsed.filter((item) => item && typeof item.wsName === 'string' && typeof item.slug === 'string') : [];
  } catch { return []; }
}
function recordTerminalProjectLink(session: LinuxTerminalSession, link: TerminalProjectLink): void {
  try {
    const next = listTerminalProjectLinks(session).filter((item) => item.wsName !== link.wsName);
    next.push(link);
    localStorage.setItem(linksStorageKey(session), JSON.stringify(next));
  } catch { /* best effort */ }
}
export type TerminalProjectLoadResult = { wsName: string; slug: string; uploadedFiles: number; uploadedBytes: number; skippedFiles: number };

export async function loadWorkspaceIntoTerminal(
  core: Pick<Services, 'projectStorage'>,
  session: LinuxTerminalSession,
  wsName: string,
  onProgress?: (detail: { path: string; uploaded: number; total: number; overallUploaded: number; overallTotal: number }) => void,
): Promise<TerminalProjectLoadResult> {
  const slug = normalizeProjectSlug(wsName, { fallback: 'project' });
  const provider = await core.projectStorage.getProvider(wsName);
  const all = await provider.listEntries();
  const files = all.filter((entry): entry is Extract<ProjectEntry, { kind: 'file' }> => entry.kind === 'file' && !excluded(String(entry.path)));
  const skippedFiles = all.filter((entry) => entry.kind === 'file' && excluded(String(entry.path))).length;
  const total = files.reduce((sum, entry) => sum + Number(entry.size || 0), 0);
  let overallUploaded = 0;
  let uploadedFiles = 0;
  for (const entry of files) {
    const path = String(entry.path);
    const bytes = await provider.readFile(entry.path);
    if (!bytes) continue;
    await uploadBytes(session.sessionId, `projects/${slug}/${path}`, bytes, (uploaded, fileTotal) => {
      onProgress?.({ path, uploaded, total: fileTotal, overallUploaded: overallUploaded + uploaded, overallTotal: total });
    });
    overallUploaded += bytes.byteLength;
    uploadedFiles += 1;
  }
  const local = await localManifest(core, wsName);
  const remote = await getLinuxTerminalWorkspaceManifest(session.sessionId, `projects/${slug}`);
  writeBaseline(session, wsName, slug, remoteManifestMap(remote.files, slug));
  // If a provider normalized a filename during loading, baseline against what is actually local too.
  if (!Object.keys(remoteManifestMap(remote.files, slug)).length && Object.keys(local.files).length) writeBaseline(session, wsName, slug, local.files);
  recordTerminalProjectLink(session, { wsName, slug, sourceType: 'local', loadedAt: Date.now() });
  return { wsName, slug, uploadedFiles, uploadedBytes: overallUploaded, skippedFiles };
}

export async function registerWorkspaceLink(core: Pick<Services, 'projectStorage'>, session: LinuxTerminalSession, wsName: string, sourceType = 'local'): Promise<void> {
  const slug = normalizeProjectSlug(wsName, { fallback: 'project' });
  const local = await localManifest(core, wsName);
  const remote = await getLinuxTerminalWorkspaceManifest(session.sessionId, `projects/${slug}`).catch(() => ({ root: `projects/${slug}`, files: [] }));
  const cloud = remoteManifestMap(remote.files, slug);
  const baseline: HashManifest = {};
  for (const path of new Set([...Object.keys(local.files), ...Object.keys(cloud)])) {
    if (local.files[path] && cloud[path] && local.files[path]!.sha256 === cloud[path]!.sha256) baseline[path] = local.files[path]!;
  }
  writeBaseline(session, wsName, slug, baseline);
  recordTerminalProjectLink(session, { wsName, slug, sourceType, loadedAt: Date.now() });
}

export type TerminalSyncConflict = { path: string; localHash?: string; cloudHash?: string };
export type TerminalSyncResult = {
  wsName: string;
  slug: string;
  downloaded: number;
  uploaded: number;
  deletedLocal: number;
  deletedRemote: number;
  deletionCandidates: string[];
  conflicts: TerminalSyncConflict[];
};

export async function syncWorkspaceBack(
  core: Pick<Services, 'projectStorage' | 'fileSystem'>,
  session: LinuxTerminalSession,
  wsName: string,
  options: { confirmRemoteDeletions?: boolean } = {},
): Promise<TerminalSyncResult> {
  const slug = normalizeProjectSlug(wsName, { fallback: 'project' });
  const provider = await core.projectStorage.getProvider(wsName);
  const local = await localManifest(core, wsName);
  const remoteRaw = await getLinuxTerminalWorkspaceManifest(session.sessionId, `projects/${slug}`);
  const cloud = remoteManifestMap(remoteRaw.files, slug);
  const baseline = readBaseline(session, wsName, slug)?.files || {};
  const paths = new Set([...Object.keys(baseline), ...Object.keys(local.files), ...Object.keys(cloud)]);
  let downloaded = 0, uploaded = 0, deletedLocal = 0, deletedRemote = 0;
  const deletionCandidates: string[] = [];
  const conflicts: TerminalSyncConflict[] = [];

  for (const path of paths) {
    const base = baseline[path];
    const localNow = local.files[path];
    const cloudNow = cloud[path];
    const localChanged = localNow?.sha256 !== base?.sha256;
    const cloudChanged = cloudNow?.sha256 !== base?.sha256;

    if (localNow && cloudNow) {
      if (localNow.sha256 === cloudNow.sha256) continue;
      if (base && localChanged && cloudChanged) {
        conflicts.push({ path, localHash: localNow.sha256, cloudHash: cloudNow.sha256 });
        continue;
      }
      if (cloudChanged && !localChanged) {
        const remote = await fetchLinuxTerminalFile(session.sessionId, `projects/${slug}/${path}`);
        const bytes = new Uint8Array(await remote.blob.arrayBuffer());
        await provider.writeFile(projectFilePath(path), bytes);
        downloaded += 1;
        continue;
      }
      if (localChanged && !cloudChanged) {
        const bytes = await provider.readFile(projectFilePath(path));
        if (bytes) { await uploadBytes(session.sessionId, `projects/${slug}/${path}`, bytes); uploaded += 1; }
        continue;
      }
      // Without a baseline, divergent copies are ambiguous. Never silently overwrite either side.
      if (!base) conflicts.push({ path, localHash: localNow.sha256, cloudHash: cloudNow.sha256 });
      continue;
    }

    if (!localNow && cloudNow) {
      if (base && !cloudChanged) {
        await deleteLinuxTerminalWorkspaceEntry(session.sessionId, `projects/${slug}/${path}`);
        deletedRemote += 1;
      } else if (base && cloudChanged) {
        conflicts.push({ path, cloudHash: cloudNow.sha256 });
      } else {
        const remote = await fetchLinuxTerminalFile(session.sessionId, `projects/${slug}/${path}`);
        const bytes = new Uint8Array(await remote.blob.arrayBuffer());
        await provider.writeFile(projectFilePath(path), bytes);
        downloaded += 1;
      }
      continue;
    }

    if (localNow && !cloudNow) {
      if (base && !localChanged) {
        if (options.confirmRemoteDeletions) {
          await core.fileSystem.deleteFile(WsPath.fromParts(wsName, projectFilePath(path)).wsPath);
          deletedLocal += 1;
        } else {
          deletionCandidates.push(path);
        }
      } else if (base && localChanged) {
        conflicts.push({ path, localHash: localNow.sha256 });
      } else {
        const bytes = await provider.readFile(projectFilePath(path));
        if (bytes) { await uploadBytes(session.sessionId, `projects/${slug}/${path}`, bytes); uploaded += 1; }
      }
    }
  }

  const nextLocal = await localManifest(core, wsName);
  const nextRemote = await getLinuxTerminalWorkspaceManifest(session.sessionId, `projects/${slug}`);
  const nextCloud = remoteManifestMap(nextRemote.files, slug);
  const next: HashManifest = {};
  for (const path of new Set([...Object.keys(nextLocal.files), ...Object.keys(nextCloud)])) {
    const localEntry = nextLocal.files[path]; const cloudEntry = nextCloud[path];
    if (localEntry && cloudEntry && localEntry.sha256 === cloudEntry.sha256) next[path] = localEntry;
  }
  writeBaseline(session, wsName, slug, next);
  return { wsName, slug, downloaded, uploaded, deletedLocal, deletedRemote, deletionCandidates, conflicts };
}

export function terminalSyncExclusions(): string[] {
  return [...EXCLUDED_SEGMENTS, ...EXCLUDED_TOP_LEVEL];
}
