import {
  BadRequestException,
  Body,
  Controller,
  Get,
  HttpCode,
  Param,
  Post,
  Put,
  Query,
  Req,
  UseGuards,
} from "@nestjs/common";
import { Request } from "express";
import {
  EmailNotificationServicio,
  EmailNotificationStatus,
  EmailNotificationType,
} from "@jorgecuadros/database";
import { Transform, Type } from "class-transformer";
import {
  ArrayMaxSize,
  IsArray,
  IsBoolean,
  IsEnum,
  IsInt,
  IsOptional,
  IsString,
  Max,
  Min,
} from "class-validator";
import { AuthenticatedGuard } from "../auth/authenticated.guard";
import { AbilityGuard } from "../auth/ability.guard";
import { RequireAbility } from "../auth/require-ability.decorator";
import { AuditService } from "../common/audit.service";
import { invalidEmails, SettingsService } from "../settings/settings.service";
import {
  NotificationScheduleService,
  parseSchedule,
  SCHEDULE_KINDS,
  ScheduleKind,
} from "./notification-schedule.service";
import { NotificationFlagsDto } from "./notification.types";
import { NotificationsService } from "./notifications.service";

/** Same flags for every job, query-string OR body (the PHP scripts took
 *  both via STDIN vs HTTP-CGI — we accept either for parity). */
class RunJobDto extends NotificationFlagsDto {}

class ListLogDto {
  @IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number;
  @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(200) pageSize?: number;
  @IsOptional() @IsEnum(EmailNotificationType) type?: EmailNotificationType;
  /** One or more servicios, comma-separated. The /notificaciones tabs each
   *  read their own slice of the one log: Servicios passes
   *  `CUSTOMERS,TRUST`, Pólizas passes `POLICIES`. Omitted = every servicio. */
  @IsOptional()
  @Transform(({ value }) =>
    typeof value === "string"
      ? value.split(",").map((s) => s.trim()).filter(Boolean)
      : value,
  )
  @IsEnum(EmailNotificationServicio, { each: true })
  servicio?: EmailNotificationServicio[];
  @IsOptional() @IsEnum(EmailNotificationStatus) status?: EmailNotificationStatus;
  @IsOptional() @IsEnum(["sent", "failed", "skipped", "all"]) view?: "sent" | "failed" | "skipped" | "all";
}

/** An empty array is valid and means "send no summaries" — the cap only
 *  exists so a paste accident can't write an unbounded blob. */
class AdminEmailsDto {
  @IsArray()
  @ArrayMaxSize(50)
  @IsString({ each: true })
  emails!: string[];
}

/** Cadence of one automatic envío. Ranges are re-checked by `parseSchedule`,
 *  which is also what the scheduler itself uses — the decorators here only
 *  reject wrong *types* so a bad payload fails at the edge. */
class ScheduleDto {
  @IsBoolean() enabled!: boolean;
  @IsInt() @Min(0) @Max(23) hour!: number;
  @IsInt() @Min(0) @Max(59) minute!: number;
  @IsOptional() @IsArray() @IsInt({ each: true }) weekdays?: number[];
}

function actingId(req: Request): string {
  return (req.user as { id: string }).id;
}

/**
 * HTTP surface for the mass-notification jobs. Four trigger endpoints +
 * two read endpoints (list log, stats). All mutations gated by the
 * `notification:send` ability so a STAFF user can't accidentally fire a
 * 260-mail sweep.
 */
@UseGuards(AuthenticatedGuard, AbilityGuard)
@Controller("notifications")
export class NotificationsController {
  constructor(
    private readonly svc: NotificationsService,
    private readonly audit: AuditService,
    private readonly settings: SettingsService,
    private readonly schedule: NotificationScheduleService,
  ) {}

  /* -------------------------------------------------------------- triggers */

  @Post("outstanding-payments")
  @RequireAbility("notification:send")
  @HttpCode(200)
  async runOutstanding(
    @Body() body: RunJobDto,
    @Query() query: RunJobDto,
    @Req() req: Request,
  ) {
    const flags = { ...query, ...body };
    const result = await this.svc.runOutstandingPayments(flags);
    void this.audit.log(actingId(req), "notification.outstanding.run", {
      debug: !!flags.debug,
      sent: result.sent,
      skipped: result.skipped,
      failed: result.failed,
    });
    return result;
  }

  @Post("payment-confirmation")
  @RequireAbility("notification:send")
  @HttpCode(200)
  async runPaymentConfirm(
    @Body() body: RunJobDto,
    @Query() query: RunJobDto,
    @Req() req: Request,
  ) {
    const flags = { ...query, ...body };
    const result = await this.svc.runPaymentConfirmation(flags);
    void this.audit.log(actingId(req), "notification.payment-confirm.run", {
      debug: !!flags.debug,
      sent: result.sent,
      skipped: result.skipped,
      failed: result.failed,
    });
    return result;
  }

  @Post("account-status")
  @RequireAbility("notification:send")
  @HttpCode(200)
  async runAccountStatus(
    @Body() body: RunJobDto,
    @Query() query: RunJobDto,
    @Req() req: Request,
  ) {
    const flags = { ...query, ...body };
    const result = await this.svc.runAccountStatus(flags);
    // Narrow the discriminated union to the ACCOUNT_STATUS variant before
    // pulling red/yellow/total — TS can't follow this through `await` alone.
    if (result.type === "ACCOUNT_STATUS") {
      void this.audit.log(actingId(req), "notification.account-status.run", {
        debug: !!flags.debug,
        red: result.red,
        yellow: result.yellow,
        total: result.total,
        sent: result.sent,
        skipped: result.skipped,
        failed: result.failed,
      });
    }
    return result;
  }

  @Post("trust-payment-confirmation")
  @RequireAbility("notification:send")
  @HttpCode(200)
  async runTrustConfirm(
    @Body() body: RunJobDto,
    @Query() query: RunJobDto,
    @Req() req: Request,
  ) {
    const flags = { ...query, ...body };
    const result = await this.svc.runTrustConfirmation(flags);
    void this.audit.log(actingId(req), "notification.trust-confirm.run", {
      debug: !!flags.debug,
      sent: result.sent,
      skipped: result.skipped,
      failed: result.failed,
    });
    return result;
  }

  /**
   * Run all four jobs sequentially with one set of flags. Audited as a
   * single `notification.run-all.run` entry carrying the aggregate totals
   * plus each job's outcome — the per-job endpoints are NOT re-audited, so
   * the log has exactly one row per staff click.
   */
  @Post("run-all")
  @RequireAbility("notification:send")
  @HttpCode(200)
  async runAll(
    @Body() body: RunJobDto,
    @Query() query: RunJobDto,
    @Req() req: Request,
  ) {
    const flags = { ...query, ...body };
    const result = await this.svc.runAll(flags);
    void this.audit.log(actingId(req), "notification.run-all.run", {
      debug: !!flags.debug,
      ignoreDayRestriction: !!flags.ignoreDayRestriction,
      useEmailLimit: !!flags.useEmailLimit,
      sent: result.sent,
      skipped: result.skipped,
      failed: result.failed,
      errors: result.errors,
      jobs: result.jobs.map((j) => ({ kind: j.kind, ok: j.ok })),
    });
    return result;
  }

  /* ----------------------------------------------------------- read views */

  @Get("log")
  listLog(@Query() q: ListLogDto) {
    const page = q.page ?? 1;
    const pageSize = q.pageSize ?? 50;
    return this.svc.listLog({
      page,
      pageSize,
      type: q.type,
      servicio: q.servicio,
      status: this.mapViewStatus(q.view, q.status),
      customerId: undefined,
    });
  }

  @Get("stats")
  stats(@Query() q: ListLogDto) {
    return this.svc.stats(q.servicio);
  }

  /* -------------------------------------------------------------- settings */

  /** Who receives the per-job summary email. Readable by any logged-in user
   *  so the UI can show the current list; editing needs `setting:manage`. */
  @Get("settings/admin-emails")
  adminEmails() {
    return this.settings.notificationAdminEmails();
  }

  @Put("settings/admin-emails")
  @RequireAbility("setting:manage")
  async setAdminEmails(@Body() dto: AdminEmailsDto, @Req() req: Request) {
    const emails = dto.emails.map((e) => e.trim()).filter(Boolean);
    const bad = invalidEmails(emails);
    if (bad.length) {
      throw new BadRequestException(
        `Correo inválido: ${bad.join(", ")}`,
      );
    }
    const result = await this.settings.setNotificationAdminEmails(
      emails,
      actingId(req),
    );
    void this.audit.log(actingId(req), "notification.settings.admin-emails", {
      emails,
    });
    return result;
  }

  /* -------------------------------------------------------------- schedule */

  /**
   * Cadence of both automatic envíos. Readable by any logged-in user so the
   * screen can show "próxima corrida" without needing edit rights; changing
   * it needs `setting:manage`, same as the summary recipients.
   */
  @Get("settings/schedule")
  schedules() {
    return this.schedule.getAll();
  }

  @Put("settings/schedule/:kind")
  @RequireAbility("setting:manage")
  async setSchedule(
    @Param("kind") kind: string,
    @Body() dto: ScheduleDto,
    @Req() req: Request,
  ) {
    if (!SCHEDULE_KINDS.includes(kind as ScheduleKind)) {
      throw new BadRequestException(
        `Horario desconocido: ${kind}. Use ${SCHEDULE_KINDS.join(" o ")}.`,
      );
    }
    const parsed = parseSchedule({ ...dto, weekdays: dto.weekdays ?? [] });
    if (!parsed.ok) throw new BadRequestException(parsed.error);

    const result = await this.schedule.set(
      kind as ScheduleKind,
      parsed.value,
      actingId(req),
    );
    void this.audit.log(actingId(req), "notification.settings.schedule", {
      kind,
      ...parsed.value,
      cron: result.cron,
    });
    return result;
  }

  /** Resolve the UI's coarse view tabs to concrete statuses. An explicit
   *  `status` wins. "Omitidos" covers both SKIPPED_* variants, which is why
   *  this returns a list rather than a single value. */
  private mapViewStatus(
    view: ListLogDto["view"],
    status: ListLogDto["status"],
  ): EmailNotificationStatus[] | undefined {
    if (status) return [status];
    if (!view || view === "all") return undefined;
    if (view === "sent") return [EmailNotificationStatus.SENT];
    if (view === "failed") return [EmailNotificationStatus.FAILED];
    if (view === "skipped") {
      return [
        EmailNotificationStatus.SKIPPED_NO_EMAIL,
        EmailNotificationStatus.SKIPPED_GATE,
      ];
    }
    return undefined;
  }
}
