import type {
  CreateBackupRequest,
  CreateBackupResponse,
  Logger,
  RestoreBackupRequest,
  RestoreBackupResponse,
  UploadPartsRequest,
  UploadPartsResponse
} from '@repo/shared';
import { ErrorCode, Operation } from '@repo/shared/errors';

import type { RequestContext } from '../core/types';
import type { BackupService } from '../services/backup-service';
import { BACKUP_WORK_DIR } from '../services/backup-service';
import { BaseHandler } from './base-handler';

type CreateBackupRequestBody = CreateBackupRequest;
const BACKUP_ALLOWED_COMPRESSIONS = ['gzip', 'lz4', 'zstd'] as const;

export class BackupHandler extends BaseHandler<Request, Response> {
  constructor(
    private backupService: BackupService,
    logger: Logger
  ) {
    super(logger);
  }

  async handle(request: Request, context: RequestContext): Promise<Response> {
    const url = new URL(request.url);
    const pathname = url.pathname;

    switch (pathname) {
      case '/api/backup/create':
        return await this.handleCreate(request, context);
      case '/api/backup/restore':
        return await this.handleRestore(request, context);
      case '/api/backup/upload-parts':
        return await this.handleUploadParts(request, context);
      default:
        return this.createErrorResponse(
          {
            message: 'Invalid backup endpoint',
            code: ErrorCode.UNKNOWN_ERROR
          },
          context
        );
    }
  }

  /** Maximum path length (matches Linux PATH_MAX) to prevent DoS via oversized strings */
  private static readonly MAX_PATH_LENGTH = 4096;

  /**
   * Validate directory path for safety (defense-in-depth).
   * Returns error message if invalid, undefined if valid.
   */
  private static validateDirPath(dir: string): string | undefined {
    if (!dir || typeof dir !== 'string') {
      return 'Missing or invalid field: dir';
    }
    if (dir.length > BackupHandler.MAX_PATH_LENGTH) {
      return 'dir path exceeds maximum length';
    }
    if (!dir.startsWith('/')) {
      return 'dir must be an absolute path';
    }
    if (dir.includes('..')) {
      return 'dir must not contain path traversal sequences';
    }
    if (dir.includes('\0')) {
      return 'dir must not contain null bytes';
    }
    return undefined;
  }

  /**
   * Validate archive path for safety.
   * Archives must be in the designated backup directory and contain no traversal.
   */
  private static validateArchivePath(archivePath: string): string | undefined {
    if (!archivePath || typeof archivePath !== 'string') {
      return 'Missing or invalid field: archivePath';
    }
    if (archivePath.length > BackupHandler.MAX_PATH_LENGTH) {
      return 'archivePath exceeds maximum length';
    }
    if (archivePath.includes('..')) {
      return 'archivePath must not contain path traversal sequences';
    }
    if (!archivePath.startsWith(`${BACKUP_WORK_DIR}/`)) {
      return 'Invalid archivePath: must use designated backup directory';
    }
    return undefined;
  }

  private static validateCompression(compression: unknown): string | undefined {
    if (compression === undefined) {
      return undefined;
    }
    if (typeof compression !== 'object' || compression === null) {
      return 'compression must be an object';
    }
    const candidate = compression as {
      format?: unknown;
      threads?: unknown;
    };
    if (
      candidate.format !== undefined &&
      (typeof candidate.format !== 'string' ||
        !BACKUP_ALLOWED_COMPRESSIONS.includes(
          candidate.format as (typeof BACKUP_ALLOWED_COMPRESSIONS)[number]
        ))
    ) {
      return `compression.format must be one of: ${BACKUP_ALLOWED_COMPRESSIONS.join(', ')}`;
    }
    if (
      candidate.threads !== undefined &&
      (typeof candidate.threads !== 'number' ||
        !Number.isInteger(candidate.threads) ||
        candidate.threads < 1)
    ) {
      return 'compression.threads must be a positive integer';
    }
    return undefined;
  }

  private static validateUploadPart(part: unknown): string | undefined {
    if (typeof part !== 'object' || part === null) {
      return 'each part must be an object';
    }

    const candidate = part as {
      partNumber?: unknown;
      url?: unknown;
      offset?: unknown;
      size?: unknown;
    };

    if (
      typeof candidate.partNumber !== 'number' ||
      !Number.isInteger(candidate.partNumber) ||
      candidate.partNumber < 1
    ) {
      return 'partNumber must be a positive integer';
    }
    if (typeof candidate.url !== 'string') {
      return 'part url must be a string';
    }
    try {
      const url = new URL(candidate.url);
      if (url.protocol !== 'https:') {
        return 'part url must use https';
      }
    } catch {
      return 'part url must be a valid URL';
    }
    if (
      typeof candidate.offset !== 'number' ||
      !Number.isInteger(candidate.offset) ||
      candidate.offset < 0
    ) {
      return 'part offset must be a non-negative integer';
    }
    if (
      typeof candidate.size !== 'number' ||
      !Number.isInteger(candidate.size) ||
      candidate.size < 1
    ) {
      return 'part size must be a positive integer';
    }
    return undefined;
  }

  private async handleCreate(
    request: Request,
    context: RequestContext
  ): Promise<Response> {
    const body = await this.parseRequestBody<CreateBackupRequestBody>(request);

    const dirError = BackupHandler.validateDirPath(body.dir);
    if (dirError) {
      return this.createErrorResponse(
        {
          message: dirError,
          code: ErrorCode.INVALID_BACKUP_CONFIG
        },
        context,
        Operation.BACKUP_CREATE
      );
    }
    const archiveError = BackupHandler.validateArchivePath(body.archivePath);
    if (archiveError) {
      return this.createErrorResponse(
        {
          message: archiveError,
          code: ErrorCode.INVALID_BACKUP_CONFIG
        },
        context,
        Operation.BACKUP_CREATE
      );
    }

    if (body.gitignore !== undefined && typeof body.gitignore !== 'boolean') {
      return this.createErrorResponse(
        {
          message: 'gitignore must be a boolean',
          code: ErrorCode.INVALID_BACKUP_CONFIG
        },
        context,
        Operation.BACKUP_CREATE
      );
    }

    if (body.excludes !== undefined) {
      if (
        !Array.isArray(body.excludes) ||
        !body.excludes.every((e) => typeof e === 'string')
      ) {
        return this.createErrorResponse(
          {
            message: 'excludes must be an array of strings',
            code: ErrorCode.INVALID_BACKUP_CONFIG
          },
          context,
          Operation.BACKUP_CREATE
        );
      }
    }

    const compressionError = BackupHandler.validateCompression(
      body.compression
    );
    if (compressionError) {
      return this.createErrorResponse(
        {
          message: compressionError,
          code: ErrorCode.INVALID_BACKUP_CONFIG
        },
        context,
        Operation.BACKUP_CREATE
      );
    }

    const sessionId = body.sessionId ?? context.sessionId ?? 'default';

    const result = await this.backupService.createArchive(
      body.dir,
      body.archivePath,
      sessionId,
      body.gitignore ?? false,
      body.excludes ?? [],
      body.compression
    );

    if (result.success) {
      const response: CreateBackupResponse = {
        success: true,
        sizeBytes: result.data.sizeBytes,
        archivePath: result.data.archivePath
      };
      return this.createTypedResponse(response, context);
    }

    return this.createErrorResponse(
      result.error,
      context,
      Operation.BACKUP_CREATE
    );
  }

  private async handleUploadParts(
    request: Request,
    context: RequestContext
  ): Promise<Response> {
    const body = await this.parseRequestBody<UploadPartsRequest>(request);

    const archiveError = BackupHandler.validateArchivePath(body.archivePath);
    if (archiveError) {
      return this.createErrorResponse(
        {
          message: archiveError,
          code: ErrorCode.INVALID_BACKUP_CONFIG
        },
        context,
        Operation.BACKUP_CREATE
      );
    }

    if (!Array.isArray(body.parts) || body.parts.length === 0) {
      return this.createErrorResponse(
        {
          message: 'parts must be a non-empty array',
          code: ErrorCode.INVALID_BACKUP_CONFIG
        },
        context,
        Operation.BACKUP_CREATE
      );
    }

    for (const part of body.parts) {
      const partError = BackupHandler.validateUploadPart(part);
      if (partError) {
        return this.createErrorResponse(
          {
            message: partError,
            code: ErrorCode.INVALID_BACKUP_CONFIG
          },
          context,
          Operation.BACKUP_CREATE
        );
      }
    }

    const sessionId = body.sessionId ?? context.sessionId ?? 'default';
    const result = await this.backupService.uploadParts(
      body.archivePath,
      body.parts,
      sessionId
    );

    if (result.success) {
      const response: UploadPartsResponse = {
        success: true,
        parts: result.data.parts
      };
      return this.createTypedResponse(response, context);
    }

    return this.createErrorResponse(
      result.error,
      context,
      Operation.BACKUP_CREATE
    );
  }

  private async handleRestore(
    request: Request,
    context: RequestContext
  ): Promise<Response> {
    const body = await this.parseRequestBody<RestoreBackupRequest>(request);

    const dirError = BackupHandler.validateDirPath(body.dir);
    if (dirError) {
      return this.createErrorResponse(
        {
          message: dirError,
          code: ErrorCode.INVALID_BACKUP_CONFIG
        },
        context,
        Operation.BACKUP_RESTORE
      );
    }
    const archiveError = BackupHandler.validateArchivePath(body.archivePath);
    if (archiveError) {
      return this.createErrorResponse(
        {
          message: archiveError,
          code: ErrorCode.INVALID_BACKUP_CONFIG
        },
        context,
        Operation.BACKUP_RESTORE
      );
    }

    const sessionId = body.sessionId ?? context.sessionId ?? 'default';

    const result = await this.backupService.restoreArchive(
      body.dir,
      body.archivePath,
      sessionId
    );

    if (result.success) {
      const response: RestoreBackupResponse = {
        success: true,
        dir: result.data.dir
      };
      return this.createTypedResponse(response, context);
    }

    return this.createErrorResponse(
      result.error,
      context,
      Operation.BACKUP_RESTORE
    );
  }
}
