import { z } from "zod/v4";
import {
  ConfigEtag,
  ContentHash,
  EffectiveWorkerStatus,
  IanaTimezone,
  LlmProtocol,
  ProcessStatus,
  ProxyProtocol,
  RuntimeStatus,
  SocialPlatform,
  TaskRunStatus,
  TaskSource,
  Uuid,
} from "./domain.js";
import { OmniFlowSessionSummarySchema } from "./omni-flow-session.js";

/**
 * REST contracts — request bodies and response shapes for the Workers HTTP API.
 *
 * Keep request types narrow (only what clients actually send); response types
 * carry derived fields like timestamps and computed status.
 */

/* ============================================================================
 * Bootstrap (dp-worker → center at startup)
 *
 * dp-worker POSTs with Authorization: Bearer <worker_api_key>, no body required;
 * center returns the full operational config + optional session blob pointer.
 *
 * [FIX from architecture.md §dp-worker-D] center must never include `org` in the response,
 * even if the DB has it for some reason. The dp-worker config schema strips it.
 * ============================================================================ */

export const SocialAccountSchema = z.object({
  id: Uuid,
  platform: SocialPlatform,
  profile_name: z.string().default(""),
  login_type: z.enum(["password", "oauth"]).default("password"),
  /** Platform handle; was historically called `handle`. */
  username: z.string().optional(),
  email: z.string().optional(),
  password: z.string().optional(),
  /**
   * Already resolved by center: when the row's persona_text is NULL the
   * bootstrap fills it from `identity.persona_text`. dp-worker just uses
   * this string directly.
   */
  persona_text: z.string().default(""),
  persona_other_json: z.record(z.string(), z.unknown()).optional(),
  enabled: z.boolean().default(true),
});
export type SocialAccount = z.infer<typeof SocialAccountSchema>;

export const ProxyConfigSchema = z.object({
  protocol: ProxyProtocol,
  host: z.string().min(1),
  port: z.number().int().positive(),
  username: z.string().min(1),
  password: z.string().min(1),
  /**
   * IANA timezone of the proxy egress location. Required on creation in center.
   * Bootstrap returns this on `config.proxy.timezone` and ALSO copies it to
   * `config.timezone` so dp-worker can use it as the default scheduler timezone
   * (so cron "0 15 * * *" on a NYC-proxy dp-worker
   * fires at NYC 15:00 regardless of where the Worker runs).
   */
  timezone: IanaTimezone,
});
export type ProxyConfigShared = z.infer<typeof ProxyConfigSchema>;

export const LlmProfileSchema = z.object({
  protocol: LlmProtocol,
  base_url: z.string().min(1),
  api_key: z.string().min(1),
  model: z.string().min(1),
  image_generation: z
    .object({
      api_key: z.string().min(1),
      model: z.string().min(1),
      size: z.string().default("1024x1024"),
      timeout_ms: z.number().int().positive().default(120000),
    })
    .optional(),
});
export type LlmProfile = z.infer<typeof LlmProfileSchema>;

export const TelegramFrontendSchema = z.object({
  bot_token: z.string().min(1),
  allowed_users: z.array(z.number().int()),
});
export type TelegramFrontendConfig = z.infer<typeof TelegramFrontendSchema>;

export const SessionBlobSchema = z.object({
  r2_get_url: z.string().url(),
  content_hash: ContentHash,
  uploaded_at: z.string().datetime(),
  size_bytes: z.number().int().positive(),
});
export type SessionBlob = z.infer<typeof SessionBlobSchema>;

export const WorkerBootstrapResponseSchema = z.object({
  config: z.object({
    worker_id: Uuid,
    name: z.string().min(1),
    config_etag: ConfigEtag,
    llm: LlmProfileSchema,
    proxy: ProxyConfigSchema.optional(),
    /**
     * Effective timezone for this dp-worker. Derived from `proxy.timezone` —
     * since v2.5 the dp-worker row no longer has its own timezone column, and
     * proxy.timezone is the single source of truth. Undefined only
     * when no proxy is bound (debug mode); dp-worker falls back to system tz
     * in that case.
     */
    timezone: z.string().optional(),
    /**
     * Chrome headless flag (M6). center-managed: the Web UI controls this
     * per dp-worker. The host CLI flag `--headless` is now only a fallback
     * for local-mode dp-workers that don't go through center bootstrap.
     */
    headless: z.boolean().default(false),
    /**
     * Browser automation backend for this dp-worker. "chrome" (default) spawns
     * real Google Chrome + CDP attach with the OmniFlow extension. "cloakbrowser"
     * launches the C++-patched stealth Chromium via the cloakbrowser package,
     * with OmniFlow loaded in passive (session-only) mode. cs-only; the host CLI
     * flag `--browser-backend` is a fallback for non-bootstrap local mode.
     */
    browser_automation_mode: z.enum(["chrome", "cloakbrowser"]).default("chrome"),
    social_accounts: z.array(SocialAccountSchema).default([]),
    telegram: TelegramFrontendSchema.optional(),
    /**
     * Limits that center enforces but dp-worker also caches for runtime decisions
     * (max iterations, task timeout, etc.).
     */
    limits: z
      .object({
        max_iterations: z.number().int().positive().default(90),
        task_timeout_ms: z.number().int().positive().default(30 * 60 * 1000),
        chrome_max_runtime_ms: z
          .number()
          .int()
          .positive()
          .default(24 * 60 * 60 * 1000),
      })
      .default(() => ({
        max_iterations: 90,
        task_timeout_ms: 30 * 60 * 1000,
        chrome_max_runtime_ms: 24 * 60 * 60 * 1000,
      })),
  }),
  session_blob: SessionBlobSchema.optional(),
});
export type WorkerBootstrapResponse = z.infer<typeof WorkerBootstrapResponseSchema>;

/* ============================================================================
 * dp-director register handshake
 * ============================================================================ */

export const DirectorRegisterHandshakeRequestSchema = z.object({
  version: z.string().min(1),
  host_label: z.string().min(1),
});

export const DirectorRegisterHandshakeResponseSchema = z.object({
  director_id: Uuid,
  short_token: z.string().min(1),
  ws_url: z.string().url(),
  expires_at: z.string().datetime(),
});
export type DirectorRegisterHandshakeResponse = z.infer<
  typeof DirectorRegisterHandshakeResponseSchema
>;

/* ============================================================================
 * R2 presigned URL flows
 * ============================================================================ */

export const SessionUploadPresignRequestSchema = z.object({
  size_bytes: z.number().int().positive(),
  content_hash: ContentHash,
});
export type SessionUploadPresignRequest = z.infer<
  typeof SessionUploadPresignRequestSchema
>;

export const SessionUploadPresignResponseSchema = z.object({
  upload_url: z.string().url(),
  object_key: z.string().min(1),
  expires_at: z.string().datetime(),
});
export type SessionUploadPresignResponse = z.infer<
  typeof SessionUploadPresignResponseSchema
>;

export const SessionFinalizeRequestSchema = z.object({
  object_key: z.string().min(1),
  content_hash: ContentHash,
  size_bytes: z.number().int().positive(),
  note: z.string().optional(),
  session_preview: OmniFlowSessionSummarySchema.optional(),
});
export type SessionFinalizeRequest = z.infer<
  typeof SessionFinalizeRequestSchema
>;

export const SessionFinalizeResponseSchema = z.object({
  id: Uuid,
  content_hash: ContentHash,
  new_config_etag: ConfigEtag.optional(),
});
export type SessionFinalizeResponse = z.infer<
  typeof SessionFinalizeResponseSchema
>;

export const AttachmentUploadPresignRequestSchema = z.object({
  filename: z.string().min(1),
  mime: z.string().optional(),
  size_bytes: z.number().int().positive(),
});

export const AttachmentUploadPresignResponseSchema =
  SessionUploadPresignResponseSchema;

export const AttachmentFinalizeRequestSchema = z.object({
  object_key: z.string().min(1),
  filename: z.string().min(1),
  mime: z.string().optional(),
  size_bytes: z.number().int().positive(),
});
export type AttachmentFinalizeRequest = z.infer<
  typeof AttachmentFinalizeRequestSchema
>;

export const AttachmentSchema = z.object({
  id: Uuid,
  filename: z.string().min(1),
  mime: z.string().optional(),
  size_bytes: z.number().int().positive(),
  created_at: z.string().datetime(),
  download_url: z.string().url().optional(),
});
export type Attachment = z.infer<typeof AttachmentSchema>;

/* ============================================================================
 * Tasks
 * ============================================================================ */

export const AdhocTaskTargetSchema = z
  .object({
    worker_ids: z.array(Uuid).default([]),
    group_ids: z.array(Uuid).default([]),
  })
  .refine((v) => v.worker_ids.length + v.group_ids.length > 0, {
    message: "must target at least one dp-worker or group",
  });

export const AdhocTaskRequestSchema = z.object({
  name: z.string().min(1),
  prompt: z.string().min(1),
  target: AdhocTaskTargetSchema,
  /**
   * 0 means dispatch immediately unless dispatch_window_seconds is set.
   * >0 picks rand(0, random_delay_seconds) per target only when no dispatch
   * window is set.
   */
  random_delay_seconds: z.number().int().nonnegative().default(0),
  /** 0 means no spreading; >0 spreads targets evenly over this dispatch window. */
  dispatch_window_seconds: z.number().int().nonnegative().default(0),
  priority: z.number().int().default(100),
});
export type AdhocTaskRequest = z.infer<typeof AdhocTaskRequestSchema>;

export const AdhocTaskBatchResponseSchema = z.object({
  batch_id: Uuid,
  task_run_ids: z.array(Uuid).min(1),
});

export const TaskRunSummarySchema = z.object({
  id: Uuid,
  worker_id: Uuid,
  scheduled_task_id: Uuid.optional(),
  source: TaskSource,
  status: TaskRunStatus,
  dispatched_at: z.string().datetime().optional(),
  started_at: z.string().datetime().optional(),
  finished_at: z.string().datetime().optional(),
  error: z.string().optional(),
  result_summary: z.string().optional(),
  parent_adhoc_task_id: Uuid.optional(),
});
export type TaskRunSummary = z.infer<typeof TaskRunSummarySchema>;

export const TaskRunListResponseSchema = z.object({
  items: z.array(TaskRunSummarySchema),
  next_cursor: z.string().optional(),
});

/* ============================================================================
 * Scheduled tasks CRUD
 * ============================================================================ */

export const ScheduledTaskBodySchema = z.object({
  name: z.string().min(1),
  cron: z.string().min(1),
  timezone: z.string().optional(),
  prompt: z.string().min(1),
  random_delay_seconds: z.number().int().nonnegative().default(0),
  enabled: z.boolean().default(true),
});
export type ScheduledTaskBody = z.infer<typeof ScheduledTaskBodySchema>;

export const ScheduledTaskResponseSchema = ScheduledTaskBodySchema.extend({
  id: Uuid,
  worker_id: Uuid,
  last_run_at: z.string().datetime().optional(),
  last_status: z.enum(["ok", "error"]).optional(),
  run_count: z.number().int().nonnegative().default(0),
  success_count: z.number().int().nonnegative().default(0),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
});

/* ============================================================================
 * Chat
 * ============================================================================ */

export const ChatSendRequestSchema = z
  .object({
    content: z.string().default(""),
    attachment_ids: z.array(Uuid).default([]),
  })
  .refine(
    (body) => body.content.trim().length > 0 || body.attachment_ids.length > 0,
    {
      message: "content or attachment_ids required",
      path: ["content"],
    }
  );

export const ChatHistoryItemSchema = z.object({
  id: Uuid,
  chat_id: Uuid,
  role: z.string(),
  content_json: z.unknown(),
  attachment_ids: z.array(Uuid).default([]),
  attachments: z.array(AttachmentSchema).default([]),
  task_run_id: Uuid.optional(),
  sender_user_id: Uuid.optional(),
  created_at: z.string().datetime(),
});

export const ChatHistoryResponseSchema = z.object({
  items: z.array(ChatHistoryItemSchema),
  next_cursor: z.string().optional(),
});

/* ============================================================================
 * Groups CRUD (M7 Phase A)
 * ============================================================================ */

export const GroupBodySchema = z.object({
  name: z.string().min(1).max(120),
  description: z.string().max(1000).optional(),
});
export type GroupBody = z.infer<typeof GroupBodySchema>;

export const GroupPatchSchema = z
  .object({
    name: z.string().min(1).max(120).optional(),
    description: z.string().max(1000).nullable().optional(),
  })
  .refine((v) => Object.keys(v).length > 0, {
    message: "patch body must contain at least one field",
  });
export type GroupPatch = z.infer<typeof GroupPatchSchema>;

export const GroupMemberMutationSchema = z.object({
  worker_ids: z.array(Uuid).min(1).max(200),
});
export type GroupMemberMutation = z.infer<typeof GroupMemberMutationSchema>;

export const GroupListItemSchema = z.object({
  id: Uuid,
  name: z.string(),
  description: z.string().optional(),
  member_count: z.number().int().nonnegative(),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
});

export const GroupDetailSchema = GroupListItemSchema.extend({
  member_worker_ids: z.array(Uuid),
});

/* ============================================================================
 * Status / list endpoints (subset)
 * ============================================================================ */

export const WorkerListItemSchema = z.object({
  id: Uuid,
  name: z.string(),
  director_id: Uuid.optional(),
  status: EffectiveWorkerStatus,
  current_task_id: Uuid.optional(),
  process_status: ProcessStatus.optional(),
  runtime_status: RuntimeStatus.optional(),
  process_observed_at: z.string().datetime().optional(),
  runtime_observed_at: z.string().datetime().optional(),
  last_heartbeat_at: z.string().datetime().optional(),
  effective_status: EffectiveWorkerStatus.optional(),
});

export const WorkerListResponseSchema = z.object({
  items: z.array(WorkerListItemSchema),
});
