import { type Application, type Request, type Response } from 'express';
import { type Repository } from '../db/repository.js';
import type { BrowserSessionRepo } from '../db/browser-session-repo.js';
import { convertToCron, calcNextRun, toSqliteDatetime } from '../scheduler.js';
import { type Scheduler } from '../scheduler.js';

export interface ScheduledTasksApiOptions {
  /**
   * Optional. When set, accepting browserSessionProfileId on create / update
   * verifies the profile belongs to the requesting user. Without it, the
   * field is silently dropped (legacy / no-auth deployments).
   */
  sessRepo?: BrowserSessionRepo;
  /**
   * Whether the auth subsystem is wired. When `false` (no-auth single-user
   * deployment) requests carry no `req.user`, so new scheduled tasks (and the
   * jobs the scheduler spawns from them) are owned by the stable `local` user
   * instead of NULL — keeping per-task reflection / visibility consistent with
   * the rest of the no-auth path. Defaults to `true`.
   */
  authActive?: boolean;
}

export function mountScheduledTasksApi(
  app: Application,
  repo: Repository,
  scheduler: Scheduler,
  apiOpts: ScheduledTasksApiOptions = {},
): void {
  const { sessRepo } = apiOpts;
  const noAuthOwner: string | null = (apiOpts.authActive ?? true) ? null : 'local';

  /**
   * Validate and resolve a browserSessionProfileId from a request body.
   * Returns:
   *   - { ok: true, value: number | null } when accepted (null = unset / clear).
   *   - { ok: false, error } when validation fails (caller sends 400).
   * Pass an undefined raw to skip validation entirely (PATCH "field absent" case).
   */
  function resolveBrowserSessionProfileId(
    raw: unknown,
    user: Express.User | undefined,
  ): { ok: true; value: number | null } | { ok: false; error: string } {
    if (raw === undefined) return { ok: true, value: null };
    if (raw === null || raw === '') return { ok: true, value: null };
    const n = Number(raw);
    if (!Number.isInteger(n) || n <= 0) {
      return { ok: false, error: 'browserSessionProfileId must be a positive integer' };
    }
    if (sessRepo) {
      if (!user?.id) {
        return { ok: false, error: 'browserSessionProfileId requires an authenticated user' };
      }
      const owned = sessRepo.getProfileById(n, user.id);
      if (!owned) {
        return { ok: false, error: 'browser session profile not found or not owned by you' };
      }
    }
    return { ok: true, value: n };
  }

  // 一覧取得
  app.get('/api/scheduled-tasks', async (req: Request, res: Response) => {
    try {
      const viewer = req.user as Express.User | undefined;
      const tasks = await repo.listScheduledTasks(viewer ? { viewer } : undefined);
      res.json({ tasks });
    } catch (err) {
      res.status(500).json({ error: `Failed to list scheduled tasks: ${err}` });
    }
  });

  // 詳細取得
  app.get('/api/scheduled-tasks/:id', async (req: Request, res: Response) => {
    try {
      const id = Number(req.params.id);
      const viewer = req.user as Express.User | undefined;
      const task = await repo.getScheduledTask(id, { viewer });
      if (!task) { res.status(404).json({ error: 'Not found' }); return; }
      res.json({ task });
    } catch (err) {
      res.status(500).json({ error: `Failed to get scheduled task: ${err}` });
    }
  });

  // 新規作成
  app.post('/api/scheduled-tasks', async (req: Request, res: Response) => {
    try {
      const { title, body, piece, profile, outputFormat, scheduleType, hour, minute, dayOfWeek, dayOfMonth, cronExpression: rawCron, scheduledAt } = req.body;

      // task_kind: 'agent' (default) or 'script'
      const rawTaskKind = req.body?.taskKind;
      const taskKind: 'agent' | 'script' = rawTaskKind === 'script' ? 'script' : 'agent';
      let scriptName: string | null = null;
      let scriptParams: string | null = null;
      if (taskKind === 'script') {
        const rawScriptName = req.body?.scriptName;
        if (typeof rawScriptName !== 'string' || !rawScriptName.trim()) {
          res.status(400).json({ error: 'scriptName is required when taskKind=script' });
          return;
        }
        scriptName = rawScriptName.trim();
        const rawScriptParams = req.body?.scriptParams;
        if (rawScriptParams !== undefined && rawScriptParams !== null) {
          if (typeof rawScriptParams === 'string') {
            try {
              const parsed = JSON.parse(rawScriptParams);
              if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
                throw new Error('scriptParams must be a JSON object');
              }
              scriptParams = JSON.stringify(parsed);
            } catch (err) {
              res.status(400).json({ error: `scriptParams is not valid JSON: ${(err as Error).message}` });
              return;
            }
          } else if (typeof rawScriptParams === 'object' && !Array.isArray(rawScriptParams)) {
            scriptParams = JSON.stringify(rawScriptParams);
          } else {
            res.status(400).json({ error: 'scriptParams must be a JSON object (or stringified JSON object)' });
            return;
          }
        }
      }

      if (taskKind === 'agent' && !body) { res.status(400).json({ error: 'body is required' }); return; }
      if (!scheduleType) { res.status(400).json({ error: 'scheduleType is required' }); return; }

      // Visibility extraction + validation (mirrors POST /api/local/tasks)
      const rawVisibility = req.body?.visibility ?? 'private';
      if (!['private', 'org', 'public'].includes(rawVisibility)) {
        res.status(400).json({ error: 'invalid visibility' });
        return;
      }
      const visibility = rawVisibility as 'private' | 'org' | 'public';
      const rawScopeOrgId = req.body?.visibilityScopeOrgId;
      const visibilityScopeOrgId: string | null =
        typeof rawScopeOrgId === 'string' && rawScopeOrgId.length > 0 ? rawScopeOrgId : null;
      if (visibility === 'org') {
        const orgIds = (req.user as Express.User | undefined)?.orgIds ?? [];
        if (!visibilityScopeOrgId || !orgIds.includes(visibilityScopeOrgId)) {
          res.status(400).json({ error: 'visibility_scope_org_id must be one of your orgs' });
          return;
        }
      }
      const ownerId = (req.user as Express.User | undefined)?.id ?? noAuthOwner;

      const profileBinding = resolveBrowserSessionProfileId(
        req.body?.browserSessionProfileId,
        req.user as Express.User | undefined,
      );
      if (!profileBinding.ok) {
        res.status(400).json({ error: profileBinding.error });
        return;
      }

      const cronExpr = convertToCron(scheduleType, { hour, minute, dayOfWeek, dayOfMonth, cronExpression: rawCron });

      let nextRunAt: string;
      if (scheduleType === 'once') {
        if (!scheduledAt) { res.status(400).json({ error: 'scheduledAt is required for once type' }); return; }
        nextRunAt = toSqliteDatetime(new Date(scheduledAt));
      } else {
        const next = calcNextRun(cronExpr);
        if (!next) { res.status(400).json({ error: 'Failed to calculate next run time' }); return; }
        nextRunAt = next;
      }

      const task = await repo.createScheduledTask({
        title: title || null,
        body: taskKind === 'script' ? (body ?? '') : body,
        pieceName: piece ?? 'auto',
        profile: profile ?? 'auto',
        outputFormat: outputFormat ?? 'markdown',
        cronExpression: cronExpr,
        nextRunAt,
        ownerId,
        visibility,
        visibilityScopeOrgId: visibility === 'org' ? visibilityScopeOrgId : null,
        browserSessionProfileId: profileBinding.value,
        taskKind,
        scriptName,
        scriptParams,
      });

      res.status(201).json({ task });
    } catch (err) {
      res.status(400).json({ error: `Failed to create scheduled task: ${err}` });
    }
  });

  // 編集
  app.patch('/api/scheduled-tasks/:id', async (req: Request, res: Response) => {
    try {
      const id = Number(req.params.id);
      const viewer = req.user as Express.User | undefined;
      const existing = await repo.getScheduledTask(id, { viewer });
      if (!existing) { res.status(404).json({ error: 'Not found' }); return; }
      if (viewer && viewer.role !== 'admin' && existing.ownerId !== viewer.id) {
        res.status(404).json({ error: 'Not found' });
        return;
      }

      const updates: Record<string, any> = {};

      if (req.body.title !== undefined) updates.title = req.body.title;
      if (req.body.body !== undefined) updates.body = req.body.body;
      if (req.body.piece !== undefined) updates.pieceName = req.body.piece;
      if (req.body.profile !== undefined) updates.profile = req.body.profile;
      if (req.body.outputFormat !== undefined) updates.outputFormat = req.body.outputFormat;

      // スケジュール変更
      if (req.body.scheduleType) {
        const cronExpr = convertToCron(req.body.scheduleType, {
          hour: req.body.hour,
          minute: req.body.minute,
          dayOfWeek: req.body.dayOfWeek,
          dayOfMonth: req.body.dayOfMonth,
          cronExpression: req.body.cronExpression,
        });
        updates.cronExpression = cronExpr;

        if (req.body.scheduleType === 'once' && req.body.scheduledAt) {
          updates.nextRunAt = toSqliteDatetime(new Date(req.body.scheduledAt));
        } else {
          const next = calcNextRun(cronExpr);
          if (next) updates.nextRunAt = next;
        }
      }

      // 一時停止/再開
      if (req.body.isActive !== undefined) {
        updates.isActive = req.body.isActive;
        // 再開時は next_run_at を再計算
        if (req.body.isActive && !updates.cronExpression) {
          const next = calcNextRun(existing.cronExpression);
          if (next) updates.nextRunAt = next;
        }
      }

      // Visibility 変更 (POST と同じバリデーション)
      if (req.body.visibility !== undefined) {
        const rawVisibility = req.body.visibility;
        if (!['private', 'org', 'public'].includes(rawVisibility)) {
          res.status(400).json({ error: 'invalid visibility' });
          return;
        }
        const rawScopeOrgId = req.body.visibilityScopeOrgId;
        const visibilityScopeOrgId: string | null =
          typeof rawScopeOrgId === 'string' && rawScopeOrgId.length > 0 ? rawScopeOrgId : null;
        if (rawVisibility === 'org') {
          const orgIds = viewer?.orgIds ?? [];
          if (!visibilityScopeOrgId || !orgIds.includes(visibilityScopeOrgId)) {
            res.status(400).json({ error: 'visibility_scope_org_id must be one of your orgs' });
            return;
          }
        }
        updates.visibility = rawVisibility;
        updates.visibilityScopeOrgId = rawVisibility === 'org' ? visibilityScopeOrgId : null;
      }

      // browserSessionProfileId 変更 (owner check)
      if (req.body.browserSessionProfileId !== undefined) {
        const binding = resolveBrowserSessionProfileId(req.body.browserSessionProfileId, viewer);
        if (!binding.ok) {
          res.status(400).json({ error: binding.error });
          return;
        }
        updates.browserSessionProfileId = binding.value;
      }

      // taskKind / scriptName / scriptParams (PATCH 用)
      if (req.body.taskKind !== undefined) {
        if (req.body.taskKind !== 'agent' && req.body.taskKind !== 'script') {
          res.status(400).json({ error: "taskKind must be 'agent' or 'script'" });
          return;
        }
        updates.taskKind = req.body.taskKind;
      }
      if (req.body.scriptName !== undefined) {
        if (req.body.scriptName === null || req.body.scriptName === '') {
          updates.scriptName = null;
        } else if (typeof req.body.scriptName === 'string') {
          updates.scriptName = req.body.scriptName.trim();
        } else {
          res.status(400).json({ error: 'scriptName must be a string' });
          return;
        }
      }
      if (req.body.scriptParams !== undefined) {
        if (req.body.scriptParams === null) {
          updates.scriptParams = null;
        } else if (typeof req.body.scriptParams === 'string') {
          try {
            const parsed = JSON.parse(req.body.scriptParams);
            if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
              throw new Error('scriptParams must be a JSON object');
            }
            updates.scriptParams = JSON.stringify(parsed);
          } catch (err) {
            res.status(400).json({ error: `scriptParams is not valid JSON: ${(err as Error).message}` });
            return;
          }
        } else if (typeof req.body.scriptParams === 'object' && !Array.isArray(req.body.scriptParams)) {
          updates.scriptParams = JSON.stringify(req.body.scriptParams);
        } else {
          res.status(400).json({ error: 'scriptParams must be a JSON object (or stringified JSON object)' });
          return;
        }
      }

      const updated = await repo.updateScheduledTask(id, updates);
      res.json({ task: updated });
    } catch (err) {
      res.status(400).json({ error: `Failed to update scheduled task: ${err}` });
    }
  });

  // 削除
  app.delete('/api/scheduled-tasks/:id', async (req: Request, res: Response) => {
    try {
      const id = Number(req.params.id);
      const viewer = req.user as Express.User | undefined;
      const existing = await repo.getScheduledTask(id, { viewer });
      if (!existing) { res.status(404).json({ error: 'Not found' }); return; }
      if (viewer && viewer.role !== 'admin' && existing.ownerId !== viewer.id) {
        res.status(404).json({ error: 'Not found' });
        return;
      }
      const deleted = await repo.deleteScheduledTask(id);
      if (!deleted) { res.status(404).json({ error: 'Not found' }); return; }
      res.json({ ok: true });
    } catch (err) {
      res.status(500).json({ error: `Failed to delete scheduled task: ${err}` });
    }
  });

  // 手動即時実行
  app.post('/api/scheduled-tasks/:id/trigger', async (req: Request, res: Response) => {
    try {
      const id = Number(req.params.id);
      const viewer = req.user as Express.User | undefined;
      const existing = await repo.getScheduledTask(id, { viewer });
      if (!existing) { res.status(404).json({ error: 'Not found' }); return; }
      if (viewer && viewer.role !== 'admin' && existing.ownerId !== viewer.id) {
        res.status(404).json({ error: 'Not found' });
        return;
      }
      await scheduler.executeById(id);
      res.json({ ok: true });
    } catch (err) {
      res.status(500).json({ error: `Failed to trigger scheduled task: ${err}` });
    }
  });
}
