import { HttpStatus, Injectable, Logger } from '@nestjs/common';

import { BusinessException } from '../../../common/errors/business.exception';
import { EntityNotFoundException } from '../../../common/errors/entity-not-found.exception';
import { MarketplaceRepository } from '../repositories/marketplace.repository';
import { RecipeService } from '../../recipes/services/recipe.service';
import { dslFromJson } from '../../recipes/utilities/dsl-cast.utility';
import { sandboxAnalyse } from '../utilities/sandbox-runner.utility';
import { canonicaliseDsl, verifyRecipeDslSignature } from '../utilities/signature.utility';
import {
  type MarketplaceListing,
  type MarketplaceListingStatus,
  Prisma,
} from '../../../generated/prisma';
import type { ListListingsQueryDto, PublishListingDto } from '../dto/publish-listing.dto';
import type { PaginatedListings } from '../types/marketplace.types';
import type { SandboxResult } from '../types/sandbox.types';

@Injectable()
export class MarketplaceService {
  private readonly logger = new Logger(MarketplaceService.name);

  constructor(
    private readonly repo: MarketplaceRepository,
    private readonly recipeService: RecipeService,
  ) {}

  async publish(userId: string, dto: PublishListingDto): Promise<MarketplaceListing> {
    this.logger.debug(`publish: userId=${userId} name=${dto.name}`);
    const canonical = canonicaliseDsl(dto.dsl);
    const valid = verifyRecipeDslSignature(canonical, dto.signature, dto.signaturePublicKey);
    if (!valid) {
      throw new BusinessException(
        'agent.marketplace.invalid_signature',
        'MARKETPLACE_SIGNATURE_INVALID',
        HttpStatus.UNPROCESSABLE_ENTITY,
        { name: dto.name },
      );
    }
    return this.repo.createListing({
      publisherUserId: userId,
      name: dto.name,
      description: dto.description ?? null,
      dsl: dto.dsl as Prisma.InputJsonValue,
      signaturePublicKey: dto.signaturePublicKey,
      signature: dto.signature,
      status: 'PUBLISHED',
      metadata: (dto.metadata ?? {}) as Prisma.InputJsonValue,
    });
  }

  async list(query: ListListingsQueryDto): Promise<PaginatedListings> {
    return this.repo.listPublishedListings(query);
  }

  async getById(id: string): Promise<MarketplaceListing> {
    const listing = await this.repo.findListingById(id);
    if (listing === null) {
      throw new EntityNotFoundException('MarketplaceListing', id);
    }
    return listing;
  }

  // V2 Stream 06 — publisher portal: listings owned by a user.
  async listForPublisher(
    publisherUserId: string,
    page: number,
    pageSize: number,
  ): Promise<PaginatedListings> {
    this.logger.debug(`listForPublisher: publisherUserId=${publisherUserId}`);
    const result = await this.repo.listForPublisher(publisherUserId, page, pageSize);
    return { ...result, page, pageSize };
  }

  // V2 Stream 06 — publisher portal: change a listing's status (hide / republish).
  // Only the publisher who owns the row may change it.
  async setStatus(
    listingId: string,
    publisherUserId: string,
    status: MarketplaceListingStatus,
  ): Promise<MarketplaceListing> {
    this.logger.log(`setStatus: listingId=${listingId} status=${status}`);
    const updated = await this.repo.setListingStatus(listingId, publisherUserId, status);
    if (updated === null) {
      throw new EntityNotFoundException('MarketplaceListing', listingId);
    }
    return updated;
  }

  /**
   * Re-verifies the listing's signature on every install (defense in
   * depth — the row in the DB could in theory be tampered with by
   * privileged DB access, but the signature still has to verify).
   */
  async install(listingId: string, userId: string): Promise<MarketplaceListing> {
    const listing = await this.getById(listingId);
    const canonical = canonicaliseDsl(listing.dsl);
    if (!verifyRecipeDslSignature(canonical, listing.signature, listing.signaturePublicKey)) {
      throw new BusinessException(
        'agent.marketplace.signature_revalidation_failed',
        'MARKETPLACE_SIGNATURE_REVALIDATION_FAILED',
        HttpStatus.UNPROCESSABLE_ENTITY,
        { listingId },
      );
    }
    // Defense in depth — even a legitimately-signed recipe goes through
    // static analysis + worker_threads dry-run before install.
    const sandbox = await sandboxAnalyse(dslFromJson(listing.dsl));
    if (sandbox.status !== 'OK') {
      throw new BusinessException(
        'agent.marketplace.sandbox_blocked',
        'MARKETPLACE_SANDBOX_BLOCKED',
        HttpStatus.UNPROCESSABLE_ENTITY,
        {
          listingId,
          sandboxStatus: sandbox.status,
          findings: [...sandbox.staticFindings, ...sandbox.runtimeFindings],
          error: sandbox.error,
        },
      );
    }
    // Materialise as a Recipe row in the user's library so they can run it.
    const dsl = dslFromJson(listing.dsl);
    const recipeName = `${listing.name} (marketplace)`;
    const recipe = await this.recipeService
      .create(userId, {
        name: recipeName,
        description: listing.description ?? `Installed from marketplace listing ${listingId}`,
        dsl,
        isEnabled: true,
        metadata: { marketplaceListingId: listingId },
      })
      .catch((error) => {
        this.logger.warn(
          `install: recipe create failed for ${listingId}: ${(error as Error).message}`,
        );
        return null;
      });
    await this.repo.incrementInstalls(listingId);
    await this.repo.recordInstall(listingId, userId, recipe?.id ?? null);
    this.logger.log(
      `install: listing ${listingId} by user ${userId} sandbox=OK recipeId=${recipe?.id ?? 'none'}`,
    );
    return listing;
  }

  /**
   * Public sandbox-analyse endpoint: lets the frontend show the user
   * what the static analyser found BEFORE they confirm install.
   */
  async analyseListing(listingId: string): Promise<SandboxResult> {
    const listing = await this.getById(listingId);
    return sandboxAnalyse(dslFromJson(listing.dsl));
  }
}
