import {
  existsSync,
  lstatSync,
  readFileSync,
  realpathSync,
  readdirSync,
} from 'node:fs';
import { basename, dirname, extname, join, relative } from 'node:path';
import { createHash } from 'node:crypto';
import { ApiError } from './http-transport.js';
import type { AuthProfile } from './auth.js';
import {
  capabilityWorkspacePath,
  readCapabilityWorkspaceRegistry,
  updateCapabilityHubIdentity,
  setCapabilityConflictChoice,
  classifyStrategyLifecycle,
  type CapabilityWorkspaceEntry,
  type CapabilityWorkspaceType,
} from './capability-workspace.js';
import {
  HubClient,
  type HubSourceFileUpload,
  type HubUploadedSourceFile,
  type ResourceType,
} from './hub-client.js';
import type { LocalAgentWorkspaceSnapshot } from './agent-workspace-sync.js';
import { readSkillName } from './skill-host.js';
import { createStoredZip, zipContentDigest, type ZipStoreEntry } from './zip-store.js';
import {
  inspectClawclawBehaviorSource,
} from '../runtime/clawclaw/plugins/behavior/behavior-source.js';
import {
  listClawclawPerceptionSourcesInDirectory,
} from '../runtime/clawclaw/plugins/perception/perception-source.js';

export interface CapabilityCloudSyncChange {
  type: CapabilityWorkspaceType;
  id: string;
  action: 'create' | 'version' | 'fork';
  resource_id: string;
  version_id: string;
  workspace_item_id?: string;
}

export interface CapabilityCloudSyncConflict {
  type: CapabilityWorkspaceType;
  id: string;
  resource_id: string;
  reason: 'local_and_cloud_changed';
}

export interface CapabilityCloudSyncFailure {
  type: CapabilityWorkspaceType;
  id: string;
  action: 'create' | 'version' | 'fork' | 'equipment';
  error: string;
}

export interface CapabilityCloudSyncSkip {
  type: CapabilityWorkspaceType;
  id: string;
  reason: 'local_origin_unrecognized' | 'fork_relationship_unresolved';
}

export interface CapabilityCloudSyncResult {
  equipmentChanged?: boolean;
  uploaded: CapabilityCloudSyncChange[];
  conflicts: CapabilityCloudSyncConflict[];
  failed: CapabilityCloudSyncFailure[];
  skipped: CapabilityCloudSyncSkip[];
  unchanged: Array<{ type: CapabilityWorkspaceType; id: string }>;
}

interface CapabilityCloudClient {
  uploadFiles(fileType: string, files: readonly HubSourceFileUpload[]): Promise<readonly HubUploadedSourceFile[]>;
  createPrivateResource(input: Parameters<HubClient['createPrivateResource']>[0]): ReturnType<HubClient['createPrivateResource']>;
  createPrivateVersion(input: Parameters<HubClient['createPrivateVersion']>[0]): ReturnType<HubClient['createPrivateVersion']>;
  installResource(resourceId: string): ReturnType<HubClient['installResource']>;
  getResource(type: ResourceType, id: string): ReturnType<HubClient['getResource']>;
  findWorkspaceItem(type: ResourceType, resourceId: string): ReturnType<HubClient['findWorkspaceItem']>;
  updateWorkspaceEquipment(
    workspaceItemId: string,
    equipped: boolean,
  ): ReturnType<HubClient['updateWorkspaceEquipment']>;
  updateWorkspaceLoadout(input: Parameters<HubClient['updateWorkspaceLoadout']>[0]):
    ReturnType<HubClient['updateWorkspaceLoadout']>;
}

export interface CapabilityCloudSyncDependencies {
  client?: CapabilityCloudClient;
  entryKeys?: readonly string[];
}

async function updateCloudEquipment(
  client: CapabilityCloudClient,
  type: CapabilityWorkspaceType,
  workspaceItemId: string,
  equipped: boolean,
): Promise<void> {
  if (type === 'persona' && equipped) {
    await client.updateWorkspaceLoadout({
      personaItemIds: [workspaceItemId],
    });
    return;
  }
  await client.updateWorkspaceEquipment(workspaceItemId, equipped);
}

function nextPatchVersion(version: string): string {
  const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
  if (!match) throw new Error(`capability_cloud_version_invalid:${version}`);
  return `${match[1]}.${match[2]}.${BigInt(match[3]!) + 1n}`;
}

function unavailableForkSource(error: unknown): boolean {
  return error instanceof ApiError && error.status === 404;
}

function resourceType(type: CapabilityWorkspaceType): ResourceType {
  return type === 'behavior' ? 'strategy' : type;
}

function sourceFileType(type: CapabilityWorkspaceType): string {
  if (type === 'behavior') return 'hub_strategy_source';
  if (type === 'perception') return 'hub_perception_source';
  if (type === 'persona') return 'hub_persona_source';
  return 'hub_skill_source';
}

function sha256(bytes: Buffer): string {
  return createHash('sha256').update(bytes).digest('hex');
}

function collectDirectory(root: string): ZipStoreEntry[] {
  const entries: ZipStoreEntry[] = [];
  const visit = (directory: string): void => {
    for (const entry of readdirSync(directory, { withFileTypes: true })) {
      const path = join(directory, entry.name);
      const stat = lstatSync(path);
      if (stat.isSymbolicLink()) throw new Error('skill_source_symlink_unsupported');
      if (stat.isDirectory()) visit(path);
      else if (stat.isFile()) entries.push({
        path: relative(root, path).replaceAll('\\', '/'),
        content: readFileSync(path),
      });
    }
  };
  visit(root);
  return entries.sort((left, right) => left.path.localeCompare(right.path));
}

function uploadPayload(entry: CapabilityWorkspaceEntry, path: string): {
  source: HubSourceFileUpload[];
  packageUpload?: HubSourceFileUpload;
  digest: string;
} {
  if (entry.type !== 'skill') {
    const bytes = readFileSync(path);
    return {
      source: [{ relativePath: basename(path), filename: basename(path), bytes }],
      digest: sha256(bytes),
    };
  }
  const files = collectDirectory(path);
  if (files.length === 0) throw new Error('skill_source_empty');
  const source = files.map((file) => ({
    relativePath: file.path,
    filename: basename(file.path),
    bytes: file.content,
  }));
  return {
    source,
    packageUpload: {
      relativePath: `${entry.runtimeId}.zip`,
      filename: `${entry.runtimeId}.zip`,
      bytes: createStoredZip(files),
    },
    digest: zipContentDigest(files),
  };
}

function snapshotResolution(
  snapshot: LocalAgentWorkspaceSnapshot,
  resourceId: string,
): string | undefined {
  return Object.values(snapshot.items).flat()
    .find((item) => item.resourceId === resourceId)?.resolution;
}

const MAX_CAPABILITY_UPLOAD_BYTES = 10 * 1024 * 1024;

function validateUploadPayload(
  entry: CapabilityWorkspaceEntry,
  path: string,
  payload: ReturnType<typeof uploadPayload>,
): void {
  const totalBytes = payload.source.reduce((total, file) => total + file.bytes.length, 0);
  if (totalBytes > MAX_CAPABILITY_UPLOAD_BYTES) {
    throw new Error('capability_validation_failed:content_too_large');
  }
  if (entry.type === 'behavior') {
    const source = inspectClawclawBehaviorSource(path, dirname(path));
    if (!source || source.id !== (entry.declaredId ?? entry.runtimeId)) {
      throw new Error('capability_validation_failed:behavior_invalid');
    }
    return;
  }
  if (entry.type === 'perception') {
    const resolvedPath = realpathSync(path);
    const source = listClawclawPerceptionSourcesInDirectory(dirname(path))
      .find((candidate) => realpathSync(candidate.sourcePath) === resolvedPath);
    if (!source || source.id !== (entry.declaredId ?? entry.runtimeId)) {
      throw new Error('capability_validation_failed:perception_invalid');
    }
    return;
  }
  if (entry.type === 'persona') {
    if (!existsSync(path) || extname(path).toLowerCase() !== '.md') {
      throw new Error('capability_validation_failed:persona_invalid');
    }
    return;
  }
  if (readSkillName(path) !== (entry.declaredId ?? entry.runtimeId)) {
    throw new Error('capability_validation_failed:skill_invalid');
  }
}

export async function syncCapabilitiesToCloud(
  workspaceDir: string,
  profile: Pick<AuthProfile, 'apiKey' | 'serverUrl'>,
  snapshot: LocalAgentWorkspaceSnapshot,
  dependencies: CapabilityCloudSyncDependencies = {},
): Promise<CapabilityCloudSyncResult> {
  const client = dependencies.client ?? HubClient.fromProfile(profile);
  const result: CapabilityCloudSyncResult = {
    uploaded: [],
    conflicts: [],
    failed: [],
    skipped: [],
    unchanged: [],
  };
  const selectedKeys = dependencies.entryKeys ? new Set(dependencies.entryKeys) : undefined;
  const unresolvedForks = new Set(Object.values(snapshot.items).flat().flatMap(item =>
    item.forkReconciliation?.status === 'unresolved'
      ? [item.resourceId, item.forkReconciliation.parentResourceId] : []));
  const entries = Object.values(readCapabilityWorkspaceRegistry(workspaceDir, profile).entries)
    .filter((entry) => (
      entry.runtimeId !== 'clawclaw'
      && !entry.hub?.cloudStatus
      && (!selectedKeys || selectedKeys.has(entry.key))
    ));
  for (const entry of entries) {
    if (entry.hub && (entry.hub.forkRelationshipUnresolved || unresolvedForks.has(entry.hub.resourceId))) {
      result.skipped.push({ type: entry.type, id: entry.runtimeId, reason: 'fork_relationship_unresolved' });
      continue;
    }
    if (entry.hub?.pendingEquipment !== undefined) {
      // An old request must never replay over a newer Hub selection. The player
      // can retry equip explicitly; load always consumes the latest cloud state.
      updateCapabilityHubIdentity(workspaceDir, profile, entry.key, {
        ...entry.hub, pendingEquipment: undefined,
      });
      delete entry.hub.pendingEquipment;
    }
    if (entry.hub?.pendingBackpackRestoreResourceId) {
      try {
        await client.installResource(entry.hub.pendingBackpackRestoreResourceId);
        updateCapabilityHubIdentity(workspaceDir, profile, entry.key, {
          ...entry.hub,
          pendingBackpackRestoreResourceId: undefined,
        });
        delete entry.hub.pendingBackpackRestoreResourceId;
      } catch (error) {
        if (unavailableForkSource(error)) updateCapabilityHubIdentity(workspaceDir, profile, entry.key, {
          ...entry.hub, pendingBackpackRestoreResourceId: undefined,
        });
        result.failed.push({
          type: entry.type,
          id: entry.runtimeId,
          action: 'fork',
          error: `fork_source_backpack_restore_failed:${error instanceof Error ? error.message : String(error)}`,
        });
        continue;
      }
    }
    const path = capabilityWorkspacePath(workspaceDir, profile, entry);
    let payload: ReturnType<typeof uploadPayload>;
    try {
      payload = uploadPayload(entry, path);
    } catch (error) {
      result.failed.push({
        type: entry.type,
        id: entry.runtimeId,
        action: entry.hub?.publisher === 'self' ? 'version' : entry.hub ? 'fork' : 'create',
        error: error instanceof Error ? error.message : String(error),
      });
      continue;
    }
    if (
      (
        entry.hub?.localOriginUnrecognized
        || (entry.hub && snapshotResolution(snapshot, entry.hub.resourceId) === 'local_origin_unrecognized')
      )
      && entry.conflictChoice !== 'local'
    ) {
      result.skipped.push({
        type: entry.type,
        id: entry.runtimeId,
        reason: 'local_origin_unrecognized',
      });
      continue;
    }
    if (
      (entry.hub?.pendingForkConflict || (entry.hub?.observedCloudVersionId
      && entry.hub.observedCloudVersionId !== entry.hub.versionId))
      && entry.conflictChoice !== 'local'
    ) {
      result.conflicts.push({
        type: entry.type,
        id: entry.runtimeId,
        resource_id: entry.hub.resourceId,
        reason: 'local_and_cloud_changed',
      });
      continue;
    }
    if (entry.hub?.lastUploadedSha256 === payload.digest || entry.hub?.baseSha256 === payload.digest) {
      result.unchanged.push({ type: entry.type, id: entry.runtimeId });
      continue;
    }
    if (
      entry.hub
      && snapshotResolution(snapshot, entry.hub.resourceId) === 'concurrent_difference'
      && entry.conflictChoice !== 'local'
    ) {
      result.conflicts.push({
        type: entry.type,
        id: entry.runtimeId,
        resource_id: entry.hub.resourceId,
        reason: 'local_and_cloud_changed',
      });
      continue;
    }
    const action = entry.hub?.publisher === 'self' ? 'version' : entry.hub ? 'fork' : 'create';
    try {
      let baseVersionId = entry.hub?.versionId;
      let versionNumber: string | undefined;
      const strategyKind = entry.type === 'behavior'
        ? classifyStrategyLifecycle(readFileSync(path, 'utf8')) : undefined;
      if (action === 'version' && entry.hub) {
        const latest = await client.getResource(resourceType(entry.type), entry.hub.resourceId);
        if (latest.versionId !== entry.hub.versionId && entry.conflictChoice !== 'local') {
          result.conflicts.push({
            type: entry.type,
            id: entry.runtimeId,
            resource_id: entry.hub.resourceId,
            reason: 'local_and_cloud_changed',
          });
          continue;
        }
        baseVersionId = latest.versionId;
        versionNumber = nextPatchVersion(latest.versionNumber ?? '1.0.0');
      }
      validateUploadPayload(entry, path, payload);
      const uploadedSource = await client.uploadFiles(sourceFileType(entry.type), payload.source);
      let packageFileId: string | undefined;
      if (payload.packageUpload) {
        const uploadedPackage = await client.uploadFiles('hub_skill_package', [payload.packageUpload]);
        packageFileId = uploadedPackage[0]?.file_id;
      }
      if (uploadPayload(entry, path).digest !== payload.digest) {
        throw new Error('capability_changed_during_upload');
      }
      const mutation = action === 'version' && entry.hub && baseVersionId
        ? await client.createPrivateVersion({
            resourceId: entry.hub.resourceId,
            baseVersionId,
            versionNumber: versionNumber!,
            strategyKind,
            title: entry.title,
            description: entry.description,
            sourceFiles: uploadedSource,
            packageFileId,
          })
        : await client.createPrivateResource({
            resourceType: resourceType(entry.type),
            strategyKind,
            title: entry.title,
            description: entry.description,
            sourceFiles: uploadedSource,
            packageFileId,
            forkFromWorkspaceItemId: action === 'fork' ? entry.hub?.workspaceItemId : undefined,
          });
      // Persist the server-confirmed resource before any further network request.
      // A failed backpack lookup must not turn the next load into another create.
      let workspaceItemId = mutation.workspaceItemId
        ?? (action === 'version' ? entry.hub?.workspaceItemId : undefined);
      const updatedHubIdentity = {
        resourceId: mutation.resourceId,
        versionId: mutation.versionId,
        workspaceItemId,
        publisher: 'self',
        baseSha256: payload.digest,
        lastUploadedSha256: payload.digest,
        observedCloudVersionId: undefined,
        pendingForkConflict: undefined,
        ...(action === 'fork' && entry.hub ? {
          pendingBackpackRestoreResourceId: entry.hub.resourceId,
        } : {}),
      } as const;
      updateCapabilityHubIdentity(workspaceDir, profile, entry.key, updatedHubIdentity);
      setCapabilityConflictChoice(workspaceDir, profile, entry.key, undefined);
      result.uploaded.push({
        type: entry.type,
        id: entry.runtimeId,
        action,
        resource_id: mutation.resourceId,
        version_id: mutation.versionId,
        ...(workspaceItemId ? { workspace_item_id: workspaceItemId } : {}),
      });
      if (!workspaceItemId) {
        try {
          const item = await client.findWorkspaceItem(resourceType(entry.type), mutation.resourceId);
          if (!item) throw new Error('capability_cloud_workspace_item_missing');
          workspaceItemId = item.workspaceItemId;
          updateCapabilityHubIdentity(workspaceDir, profile, entry.key, {
            ...updatedHubIdentity, workspaceItemId,
          });
          result.uploaded.at(-1)!.workspace_item_id = workspaceItemId;
        } catch (error) {
          result.failed.push({ type: entry.type, id: entry.runtimeId, action: 'equipment',
            error: `resource_created_workspace_lookup_failed:${error instanceof Error ? error.message : String(error)}` });
        }
      }
      if (action === 'create' && entry.origin === 'local' && entry.selection !== 'pending' && workspaceItemId) {
        try {
          await updateCloudEquipment(client, entry.type, workspaceItemId, entry.selection === 'equipped');
          result.equipmentChanged = true;
        } catch (error) {
          result.failed.push({
            type: entry.type,
            id: entry.runtimeId,
            action: 'equipment',
            error: `capability_cloud_equipment_sync_failed:${
              error instanceof Error ? error.message : String(error)
            }`,
          });
        }
      }
      if (action === 'fork' && entry.hub) {
        try {
          await client.installResource(entry.hub.resourceId);
          updateCapabilityHubIdentity(workspaceDir, profile, entry.key, {
            ...updatedHubIdentity,
            workspaceItemId,
            pendingBackpackRestoreResourceId: undefined,
          });
        } catch (error) {
          if (unavailableForkSource(error)) updateCapabilityHubIdentity(workspaceDir, profile, entry.key, {
            ...updatedHubIdentity, workspaceItemId, pendingBackpackRestoreResourceId: undefined,
          });
          result.failed.push({
            type: entry.type,
            id: entry.runtimeId,
            action: 'fork',
            error: `fork_source_backpack_restore_failed:${error instanceof Error ? error.message : String(error)}`,
          });
        }
      }
    } catch (error) {
      result.failed.push({
        type: entry.type,
        id: entry.runtimeId,
        action,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }
  return result;
}
