import express, { Router } from 'express';
import { dirname } from 'node:path';
import type { Repository } from '../db/repository.js';
import { spaceWorkspaceDir } from '../spaces/paths.js';
import { scaffoldCaseSpace, removeSpaceDirs } from '../spaces/scaffold.js';
import { canManageSpace } from './visibility.js';
import { registerSpaceFilesRoutes } from './space-files-api.js';
import { registerSpaceMembersRoutes } from './space-members-api.js';
import { registerSpaceCalendarRoutes } from './space-calendar-api.js';
import { registerSpaceInviteRoutes } from './space-invite-api.js';
import { registerSpaceAppShareRoutes } from './space-app-share-api.js';
import { registerSpaceToolPolicyRoutes } from './space-tool-policy-api.js';
import { registerSpacePythonPackagesRoutes } from './space-python-packages-api.js';
import { viewerOf } from './space-viewer.js';
import { logger } from '../logger.js';
import { sendOfficePreview, handleOfficePreviewError } from './office-preview.js';
import { parseToolPolicy } from '../engine/workspace-tool-policy.js';
import {
  listToolCategories,
  SENSITIVE_CATEGORIES,
  SENSITIVE_TOOLS,
} from '../engine/tools/tool-categories.js';
import {
  listTransferCandidates,
  executeTransfer,
} from '../services/space-credential-transfer.js';

// cross-calendar-api.ts は parseTzOffset / monthBounds を space-api 経由で参照する。
// 実体はカレンダールートと同居させたほうが凝集が高いので space-calendar-api に移し、
// 後方互換のためここで再エクスポートする（spaceViewerOf と同じ公開境界の扱い）。
export { parseTzOffset, monthBounds } from './space-calendar-api.js';

export interface SpaceApiDeps {
  repo: Repository;
  dataRoot: string;
  worktreeDir: string;
  authActive: boolean;
  /** アップロード JSON ボディの上限（MB）。未指定は 50MB（base64 は ~33% 膨張する点に注意）。 */
  uploadLimitMb?: number;
  /** Per-space Python packages のライブ config（loadConfig() を都度読む getter）。未配線 = 無効扱い。 */
  getPythonPackagesConfig?: () => import('../config.js').PythonPackagesConfigShape | undefined;
}

/**
 * 認証 OFF 環境のフォールバックユーザーを返す（cross-calendar-api 等から流用）。
 * 規約は viewerOf と同一（synthetic 'local' admin）。
 */
export function spaceViewerOf(req: any, authActive: boolean): Express.User {
  return viewerOf(req, authActive);
}

export function createSpaceApi(deps: SpaceApiDeps): Router {
  const router = Router();
  const { repo, dataRoot, worktreeDir } = deps;

  // body を読むルート（create / patch / upload）専用の json parser。
  // グローバル json は server.ts から外したので、各ルートが自前で宣言する。
  // upload を通すため 50mb（または config 指定）まで許容する。
  const jsonParser = express.json({ limit: deps.uploadLimitMb ? `${deps.uploadLimitMb}mb` : '50mb' });

  // 一覧（個人スペースは初回アクセス時に lazy 生成）。各スペースに閲覧者の myRole も付与
  // （クライアントの editor 含むファイル編集可否判定に使う。SpaceDetail は一覧から space を引く）。
  router.get('/', async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    await repo.ensurePersonalSpace(viewer.id, viewer.name ?? undefined);
    const spaces = await repo.listSpaces({ viewer });
    res.json(spaces.map(s => ({ ...s, myRole: repo.getSpaceMemberRole(s.id, viewer.id) })));
  });

  // 詳細。閲覧者のメンバーロール（myRole）も載せる。クライアントが editor を含む
  // ファイル編集可否（canEditInSpace 相当）を判定するために使う。非メンバーは null。
  router.get('/:id', async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    const space = await repo.getSpace(req.params.id, { viewer });
    if (!space) return res.status(404).json({ error: 'not found' });
    const myRole = repo.getSpaceMemberRole(space.id, viewer.id);
    res.json({ ...space, myRole });
  });

  // 作成（案件スペース）。DB 行 → 雛形作成。雛形失敗時は行を消してロールバック。
  router.post('/', jsonParser, async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    const title = (req.body?.title ?? '').toString().trim();
    if (!title) return res.status(400).json({ error: 'title is required' });
    const ownerId = viewer.id === 'local' ? null : viewer.id;
    const space = await repo.createSpace({
      kind: 'case',
      title,
      description: req.body?.description?.toString() ?? '',
      ownerId,
      visibility: req.body?.visibility ?? 'private',
      visibilityScopeOrgId: req.body?.visibilityScopeOrgId ?? null,
      brandColor: req.body?.brandColor ?? null,
    });
    try {
      // 案件スペースのフォルダ実体は data/spaces/{id}（spec §5.3）。dataRoot は
      // userFolderRoot（data/users）が渡るため、scaffold の spaceDataDir が
      // {dataRoot}/spaces/{id} を作る前提に合わせ dataRoot=dirname(userFolderRoot)=data を渡す。
      scaffoldCaseSpace({ dataRoot: dirname(dataRoot), worktreeDir, id: space.id, title });
      // workspace_dir を確定保存
      const ws = spaceWorkspaceDir(worktreeDir, space.id);
      await repo.setSpaceWorkspaceDir(space.id, ws);
      const fresh = await repo.getSpace(space.id, { viewer });
      res.status(201).json(fresh);
    } catch (err) {
      logger.error(`[space-api] scaffold failed, rolling back space=${space.id} err=${(err as Error).message}`);
      removeSpaceDirs({ dataRoot: dirname(dataRoot), worktreeDir, id: space.id });
      await repo.hardDeleteSpace(space.id);
      res.status(500).json({ error: 'failed to create space' });
    }
  });

  // 更新（owner / admin / member.role==='owner'）
  router.patch('/:id', jsonParser, async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    const space = await repo.getSpace(req.params.id, { viewer });
    if (!space) return res.status(404).json({ error: 'not found' });
    const memberRole = repo.getSpaceMemberRole(space.id, viewer.id);
    if (!canManageSpace(viewer, { ownerId: space.ownerId }, memberRole)) {
      return res.status(403).json({ error: 'forbidden' });
    }
    const updated = await repo.updateSpace(req.params.id, {
      title: req.body?.title,
      description: req.body?.description,
      brandColor: req.body?.brandColor,
      visibility: req.body?.visibility,
      visibilityScopeOrgId: req.body?.visibilityScopeOrgId,
    });
    res.json(updated);
  });

  // アーカイブ（owner / admin / member.role==='owner'、案件のみ）
  router.post('/:id/archive', async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    const space = await repo.getSpace(req.params.id, { viewer });
    if (!space) return res.status(404).json({ error: 'not found' });
    const memberRole = repo.getSpaceMemberRole(space.id, viewer.id);
    if (!canManageSpace(viewer, { ownerId: space.ownerId }, memberRole)) {
      return res.status(403).json({ error: 'forbidden' });
    }
    if (space.kind === 'personal') return res.status(400).json({ error: 'cannot archive personal space' });
    await repo.archiveSpace(req.params.id);
    res.json({ ok: true });
  });

  // Space ファイル窓ルート（/:id/files/*）は space-files-api.ts に分離（巨大関数分割）。
  registerSpaceFilesRoutes(router, deps);

  // カレンダー（予定 + 日次集計）は space-calendar-api.ts に分離（巨大関数分割）。
  registerSpaceCalendarRoutes(router, deps);

  // メンバー（協働者）CRUD は space-members-api.ts に分離（巨大関数分割）。
  registerSpaceMembersRoutes(router, deps);

  // 招待リンク（再利用トークン）は space-invite-api.ts に分離（巨大関数分割）。
  registerSpaceInviteRoutes(router, deps);

  // 公開アプリ共有リンクは space-app-share-api.ts に分離（巨大関数分割）。
  registerSpaceAppShareRoutes(router, deps);

  // ツールポリシー GET/PUT は space-tool-policy-api.ts に分離（巨大関数分割）。
  registerSpaceToolPolicyRoutes(router, deps);

  // Python パッケージ管理 GET/POST/DELETE は space-python-packages-api.ts に分離。
  registerSpacePythonPackagesRoutes(router, deps);

  // ─── スペース間資格情報コピー ──────────────────────────────────────
  // 両スペースの管理権（canManageSpace）を持つユーザーのみ利用可能。
  // 成功時に audit_log へ記録する。

  /** 閲覧者が source・target 両スペースを管理できるかを確認する。 */
  async function canManageBothSpaces(
    viewer: Express.User,
    sourceId: string,
    targetId: string,
  ): Promise<boolean> {
    const source = await repo.getSpace(sourceId, { viewer });
    const target = await repo.getSpace(targetId, { viewer });
    if (!source || !target) return false;
    const srcRole = repo.getSpaceMemberRole(sourceId, viewer.id);
    const tgtRole = repo.getSpaceMemberRole(targetId, viewer.id);
    return (
      canManageSpace(viewer, source, srcRole) &&
      canManageSpace(viewer, target, tgtRole)
    );
  }

  // GET /:targetId/transfer/candidates?sourceId=<id>
  // 元スペースの MCP サーバー・SSH 接続の転送候補一覧を返す。
  router.get('/:targetId/transfer/candidates', async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    const targetId = req.params.targetId;
    const sourceId = String(req.query.sourceId ?? '');
    if (!sourceId) return res.status(400).json({ error: 'sourceId required' });
    if (sourceId === targetId) return res.status(400).json({ error: 'source and target must differ' });
    if (!(await canManageBothSpaces(viewer, sourceId, targetId))) {
      return res.status(403).json({ error: 'forbidden' });
    }
    const db = repo.getDb();
    return res.json(listTransferCandidates({ db }, sourceId, targetId));
  });

  // POST /:targetId/transfer
  // body: { sourceId, mcpServerIds: string[], sshConnectionIds: string[] }
  // 指定した MCP サーバー・SSH 接続を転送し、copied/skipped サマリを返す。
  router.post('/:targetId/transfer', jsonParser, async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    const targetId = req.params.targetId;
    const sourceId = String(req.body?.sourceId ?? '');
    if (!sourceId) return res.status(400).json({ error: 'sourceId required' });
    if (sourceId === targetId) return res.status(400).json({ error: 'source and target must differ' });

    const mcpServerIds = req.body?.mcpServerIds;
    const sshConnectionIds = req.body?.sshConnectionIds;
    if (mcpServerIds !== undefined && !Array.isArray(mcpServerIds)) {
      return res.status(400).json({ error: 'mcpServerIds must be an array' });
    }
    if (sshConnectionIds !== undefined && !Array.isArray(sshConnectionIds)) {
      return res.status(400).json({ error: 'sshConnectionIds must be an array' });
    }

    if (!(await canManageBothSpaces(viewer, sourceId, targetId))) {
      return res.status(403).json({ error: 'forbidden' });
    }

    const db = repo.getDb();
    const result = executeTransfer(
      { db },
      {
        sourceSpaceId: sourceId,
        targetSpaceId: targetId,
        actorId: viewer.id,
        mcpServerIds: (mcpServerIds as string[]) ?? [],
        sshConnectionIds: (sshConnectionIds as string[]) ?? [],
      },
    );

    await deps.repo.addAuditLog(null, 'space.credential.transfer', viewer.id, {
      sourceId,
      targetId,
      copied: result.copied.length,
      skipped: result.skipped.length,
    });

    return res.json(result);
  });

  return router;
}
