import {
  Injectable,
  BadRequestException,
  NotFoundException,
  ForbiddenException,
  ConflictException,
  UnauthorizedException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { CheckpointRepository } from '../repositories/checkpoint.repository';
import { SharedCheckinPartnerRepository } from '../repositories/shared-checkin-partner.repository';
import { TicketUsageRepository } from '../repositories/ticket-usage.repository';
import {
  CreatePartnerDto,
  UpdatePartnerDto,
  DispatchRequestDto,
} from '../dto/shared-checkin.dto';
import {
  issueQrToken,
  validateQrToken,
  issueTicket,
  validateTicketShape,
  isAllowedHost,
  QrTokenScope,
} from '../utils/shared-checkin.util';
import { SiteAttendanceErrorCodes, COMPANY_ID_PATTERN } from '../error-codes';

@Injectable()
export class SharedCheckinService {
  constructor(
    private readonly checkpointRepo: CheckpointRepository,
    private readonly partnerRepo: SharedCheckinPartnerRepository,
    private readonly ticketUsageRepo: TicketUsageRepository,
  ) {}

  // --- QR token ---

  async getQrTokenForCheckpoint(code: string) {
    const cp = await this.checkpointRepo.findActiveByCode(code);
    if (!cp) {
      throw new NotFoundException({
        code: SiteAttendanceErrorCodes.CHECKPOINT_NOT_FOUND,
      });
    }

    // PUBLIC 无需 token — 前端直接拼静态 URL
    if (cp.accessMode === 'PUBLIC' && !cp.sharedCheckinEnabled) {
      return {
        token: '',
        expiresAt: null,
        scope: 'checkpoint' as QrTokenScope,
        rotationSeconds: null,
        graceSeconds: cp.qrGraceSeconds,
      };
    }

    const scope: QrTokenScope = cp.sharedCheckinEnabled ? 'shared' : 'checkpoint';
    const { token, expiresAt } = issueQrToken({
      scope,
      checkpointCode: cp.code,
      rotationSeconds: cp.qrRotationSeconds,
    });

    return {
      token,
      expiresAt,
      scope,
      rotationSeconds: cp.qrRotationSeconds,
      graceSeconds: cp.qrGraceSeconds,
    };
  }

  // --- Dispatch options ---

  async getDispatchOptions(code: string, qrToken: string) {
    const cp = await this.checkpointRepo.findActiveByCode(code);
    if (!cp) {
      throw new NotFoundException({
        code: SiteAttendanceErrorCodes.DISPATCH_CHECKPOINT_NOT_FOUND,
      });
    }
    if (!cp.isActive) {
      throw new NotFoundException({
        code: SiteAttendanceErrorCodes.DISPATCH_CHECKPOINT_INACTIVE,
      });
    }
    if (!cp.sharedCheckinEnabled) {
      throw new NotFoundException({
        code: SiteAttendanceErrorCodes.DISPATCH_NOT_ENABLED,
      });
    }

    this.verifyQrToken(qrToken, cp);

    const partners = await this.partnerRepo.listActiveByCheckpoint(cp.id);
    return {
      self: {
        companyId: cp.sharedCompanyId!,
        companyLabel: cp.sharedCompanyLabel!,
        checkpointCode: cp.code,
        checkpointName: cp.name,
      },
      partners: partners.map((p) => ({
        partnerId: p.id,
        companyId: p.companyId,
        companyLabel: p.companyLabel,
        displayLabel: p.displayLabel ?? undefined,
        isExternal: true,
      })),
    };
  }

  // --- Dispatch (sign ticket) ---

  async dispatch(req: DispatchRequestDto, dispatchOrigin: string) {
    const cp = await this.checkpointRepo.findActiveByCode(req.checkpointCode);
    if (!cp) {
      throw new NotFoundException({
        code: SiteAttendanceErrorCodes.DISPATCH_CHECKPOINT_NOT_FOUND,
      });
    }
    if (!cp.sharedCheckinEnabled) {
      throw new NotFoundException({
        code: SiteAttendanceErrorCodes.DISPATCH_NOT_ENABLED,
      });
    }

    this.verifyQrToken(req.qrToken, cp);

    if (!req.choice || !req.choice.companyId) {
      throw new BadRequestException({
        code: SiteAttendanceErrorCodes.DISPATCH_CHOICE_INVALID,
      });
    }

    // Self?
    if (req.choice.companyId === cp.sharedCompanyId && !req.choice.partnerId) {
      const { ticket, expiresAt } = issueTicket({
        companyId: cp.sharedCompanyId!,
        targetMode: 'local',
        targetCode: cp.code,
        targetUrl: null,
        dispatchCheckpointId: cp.id,
        dispatchOrigin,
      });
      // v1.5: 本地 self 跳转也附带 dispatchOrigin + 原始 qrToken，
      // 让签到页能显示"切换公司"按钮并把原 t 带回分诊页（防直链）
      const redirectUrl =
        `/siteattendance/c/${encodeURIComponent(cp.code)}` +
        `?ticket=${encodeURIComponent(ticket)}` +
        `&dispatchOrigin=${encodeURIComponent(dispatchOrigin)}` +
        `&t=${encodeURIComponent(req.qrToken)}`;
      return { redirectUrl, ticketExpiresAt: expiresAt, targetMode: 'local' as const };
    }

    // Partner?
    if (!req.choice.partnerId) {
      throw new BadRequestException({
        code: SiteAttendanceErrorCodes.DISPATCH_CHOICE_INVALID,
      });
    }
    const partner = await this.partnerRepo.findById(req.choice.partnerId);
    if (!partner || partner.checkpointId !== cp.id) {
      throw new BadRequestException({
        code: SiteAttendanceErrorCodes.DISPATCH_CHOICE_INVALID,
      });
    }
    if (!partner.isActive) {
      throw new BadRequestException({
        code: SiteAttendanceErrorCodes.DISPATCH_PARTNER_INACTIVE,
      });
    }
    if (partner.companyId !== req.choice.companyId) {
      throw new BadRequestException({
        code: SiteAttendanceErrorCodes.DISPATCH_CHOICE_INVALID,
      });
    }

    const { ticket, expiresAt } = issueTicket({
      companyId: partner.companyId,
      targetMode: 'external',
      targetCode: null,
      targetUrl: partner.targetUrl,
      dispatchCheckpointId: cp.id,
      dispatchOrigin,
    });
    const sep = partner.targetUrl.includes('?') ? '&' : '?';
    // v1.5: 附带原始 qrToken 让外部签到页的"切换公司"按钮能正确回跳（防直链）
    const redirectUrl =
      `${partner.targetUrl}${sep}ticket=${encodeURIComponent(ticket)}` +
      `&dispatchOrigin=${encodeURIComponent(dispatchOrigin)}` +
      `&t=${encodeURIComponent(req.qrToken)}`;
    return { redirectUrl, ticketExpiresAt: expiresAt, targetMode: 'external' as const };
  }

  // --- Ticket validation ---

  async validateTicket(ticket: string, targetCheckpointCode: string) {
    const shape = validateTicketShape(ticket);
    if (!shape.valid) {
      switch (shape.reason) {
        case 'malformed':
          throw new BadRequestException({
            code: SiteAttendanceErrorCodes.TICKET_MALFORMED,
          });
        case 'invalid':
          throw new UnauthorizedException({
            code: SiteAttendanceErrorCodes.TICKET_INVALID,
          });
        case 'expired':
          throw new UnauthorizedException({
            code: SiteAttendanceErrorCodes.TICKET_EXPIRED,
          });
        case 'origin_not_allowed':
          throw new ForbiddenException({
            code: SiteAttendanceErrorCodes.TICKET_ORIGIN_NOT_ALLOWED,
          });
      }
    }
    const payload = shape.payload!;

    // targetCode 匹配（本地跳）
    if (payload.targetMode === 'local' && payload.targetCode !== targetCheckpointCode) {
      throw new BadRequestException({
        code: SiteAttendanceErrorCodes.TICKET_TARGET_MISMATCH,
      });
    }
    // 跨域跳落到本 workspace 签到页时，targetCheckpointCode 必须是 partner URL 的最终 code
    // 由调用侧保证 targetCheckpointCode 与签到页当前 code 一致，服务端不再额外解析 partner URL

    // nonce 一次性
    try {
      await this.ticketUsageRepo.insertNonce(
        payload.nonce,
        new Date(payload.ts + 300 * 1000),
      );
    } catch (e) {
      if (
        e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2002'
      ) {
        throw new ConflictException({
          code: SiteAttendanceErrorCodes.TICKET_ALREADY_USED,
        });
      }
      throw e;
    }

    return {
      valid: true,
      payload: {
        companyId: payload.companyId,
        dispatchOrigin: payload.dispatchOrigin,
        expiresAt: payload.ts + 300 * 1000,
      },
    };
  }

  // --- Partner CRUD ---

  async listPartners(checkpointId: string) {
    await this.assertCheckpointExists(checkpointId);
    return this.partnerRepo.listByCheckpoint(checkpointId);
  }

  async createPartner(checkpointId: string, dto: CreatePartnerDto, userId: string) {
    const cp = await this.assertCheckpointExists(checkpointId);
    if (!cp.sharedCheckinEnabled) {
      throw new BadRequestException({
        code: SiteAttendanceErrorCodes.PARTNER_CHECKPOINT_NOT_SHARED,
      });
    }
    this.validatePartnerPayload(dto);
    return this.partnerRepo.create({
      checkpointId,
      companyId: dto.companyId,
      companyLabel: dto.companyLabel,
      displayLabel: dto.displayLabel ?? null,
      targetUrl: dto.targetUrl,
      isActive: dto.isActive ?? true,
      sortOrder: dto.sortOrder ?? 0,
      createdBy: userId,
      updatedBy: userId,
    });
  }

  async updatePartner(partnerId: string, dto: UpdatePartnerDto, userId: string) {
    const existing = await this.partnerRepo.findById(partnerId);
    if (!existing) {
      throw new NotFoundException({
        code: SiteAttendanceErrorCodes.PARTNER_NOT_FOUND,
      });
    }
    const merged = { ...existing, ...dto };
    this.validatePartnerPayload(merged);
    const data: Record<string, any> = {};
    if (dto.companyId !== undefined) data.companyId = dto.companyId;
    if (dto.companyLabel !== undefined) data.companyLabel = dto.companyLabel;
    if (dto.displayLabel !== undefined) data.displayLabel = dto.displayLabel;
    if (dto.targetUrl !== undefined) data.targetUrl = dto.targetUrl;
    if (dto.isActive !== undefined) data.isActive = dto.isActive;
    if (dto.sortOrder !== undefined) data.sortOrder = dto.sortOrder;
    data.updatedBy = userId;
    return this.partnerRepo.update(partnerId, data);
  }

  async deletePartner(partnerId: string) {
    const existing = await this.partnerRepo.findById(partnerId);
    if (!existing) {
      throw new NotFoundException({
        code: SiteAttendanceErrorCodes.PARTNER_NOT_FOUND,
      });
    }
    await this.partnerRepo.delete(partnerId);
  }

  // --- Maintenance ---

  cleanupTicketUsage() {
    return this.ticketUsageRepo.cleanupExpired();
  }

  // --- Private ---

  private async assertCheckpointExists(checkpointId: string) {
    const cp = await this.checkpointRepo.findById(checkpointId);
    if (!cp) {
      throw new NotFoundException({
        code: SiteAttendanceErrorCodes.CHECKPOINT_NOT_FOUND,
      });
    }
    return cp;
  }

  private validatePartnerPayload(dto: {
    companyId?: string;
    targetUrl?: string;
  }) {
    if (dto.companyId !== undefined && !COMPANY_ID_PATTERN.test(dto.companyId)) {
      throw new BadRequestException({
        code: SiteAttendanceErrorCodes.PARTNER_COMPANYID_INVALID,
      });
    }
    if (dto.targetUrl !== undefined) {
      try {
        // URL 合法性
        new URL(dto.targetUrl);
      } catch {
        throw new BadRequestException({
          code: SiteAttendanceErrorCodes.PARTNER_TARGETURL_INVALID,
        });
      }
      if (!isAllowedHost(dto.targetUrl)) {
        throw new BadRequestException({
          code: SiteAttendanceErrorCodes.PARTNER_TARGETURL_HOST_NOT_ALLOWED,
        });
      }
    }
  }

  private verifyQrToken(
    qrToken: string,
    cp: { code: string; qrRotationSeconds: number | null; qrGraceSeconds: number; sharedCheckinEnabled: boolean; accessMode: string },
  ) {
    if (cp.accessMode === 'PUBLIC' && !cp.sharedCheckinEnabled) {
      // PUBLIC 独立签到不需要 QR token；若传了也跳过
      return;
    }
    const scope: QrTokenScope = cp.sharedCheckinEnabled ? 'shared' : 'checkpoint';
    const result = validateQrToken({
      token: qrToken,
      scope,
      checkpointCode: cp.code,
      rotationSeconds: cp.qrRotationSeconds,
      graceSeconds: cp.qrGraceSeconds,
    });
    if (!result.valid) {
      switch (result.reason) {
        case 'malformed':
          throw new BadRequestException({
            code: SiteAttendanceErrorCodes.QR_TOKEN_MALFORMED,
          });
        case 'expired':
          throw new UnauthorizedException({
            code: SiteAttendanceErrorCodes.QR_TOKEN_EXPIRED,
          });
        case 'invalid':
        default:
          throw new UnauthorizedException({
            code: SiteAttendanceErrorCodes.QR_TOKEN_INVALID,
          });
      }
    }
  }
}
