import express, { Router, type Request, type Response } from 'express';
import type Provider from 'oidc-provider';
import type { Repository } from '../../db/repository.js';
import { A2A_READ_SCOPE } from './oidc-config.js';
import { logger } from '../../logger.js';
import { randomUUID } from 'crypto';

/**
 * 同意 interaction。ユーザーは既存 Passport セッションでログイン済みである必要がある（req.user）。
 * GET /:uid          → 同意画面（最小 HTML）
 * POST /:uid/confirm → Grant を保存して認可コードフローを再開
 * POST /:uid/abort   → access_denied で中断
 */
export function createConsentRouter(provider: Provider, repo: Repository, resourceAudience: string): Router {
  const router = Router();
  // マウント先の body-parser 構成によらず確実に動作するよう自前で登録する。
  // HTML フォーム (application/x-www-form-urlencoded) にも対応。
  router.use(express.urlencoded({ extended: false }));
  router.use(express.json());

  router.get('/:uid', async (req: Request, res: Response) => {
    try {
      const details = await provider.interactionDetails(req, res);
      const user = req.user as Express.User | undefined;
      if (!user) {
        // 未ログイン: 既存ログインへ。戻り先に interaction を指定。
        res.redirect(`/auth/login?returnTo=${encodeURIComponent(req.originalUrl)}`);
        return;
      }
      const client = repo.getA2aClient(details.params.client_id as string);
      const clientName = client?.name ?? (details.params.client_id as string);
      const scopes = String(details.params.scope ?? '').split(' ').filter(Boolean);

      // ユーザーが閲覧可能でかつ A2A スキルが 1 件以上あるスペースを列挙する。
      const allSpaces = await repo.listSpaces({ viewer: user });
      const spacesWithSkills: Array<{ id: string; title: string; skills: string[] }> = [];
      for (const space of allSpaces) {
        const skills = repo.getSpaceA2aSkills(space.id);
        if (skills.length > 0) spacesWithSkills.push({ id: space.id, title: space.title, skills });
      }

      res.type('html').send(renderConsent(details.uid, clientName, scopes, spacesWithSkills));
    } catch (err) {
      logger.error(`[a2a-oidc] interaction render error: ${err}`);
      res.status(500).send('interaction error');
    }
  });

  router.post('/:uid/confirm', async (req: Request, res: Response) => {
    try {
      const details = await provider.interactionDetails(req, res);
      const user = req.user as Express.User | undefined;
      if (!user) { res.status(401).json({ error: 'login required' }); return; }

      const accountId = user.id;
      const clientId = details.params.client_id as string;
      const resource = (details.params.resource as string | undefined) ?? resourceAudience;

      // ボディから要求されたスペース/スキルを取り出す（JSON or URL-encoded 両対応）。
      // 単一チェックボックスは string で届くため concat で配列に正規化する。
      const toStringArray = (v: unknown): string[] =>
        ([] as string[]).concat(v as any ?? []).filter((x): x is string => typeof x === 'string');
      const requestedSpaces = toStringArray(req.body?.selectedSpaces);
      const requestedSkillSet = new Set<string>(toStringArray(req.body?.selectedSkills));

      // サーバー側で再検証: ユーザーが実際に閲覧できるかつスキルを持つスペースのみ許可。
      const visibleSpaces = await repo.listSpaces({ viewer: user });
      const visibleSpaceIds = new Set(visibleSpaces.map(s => s.id));

      const grantedSpaceIds: string[] = [];
      const grantedSkillsSet = new Set<string>();
      for (const spaceId of requestedSpaces) {
        if (!visibleSpaceIds.has(spaceId)) continue;
        const allowlisted = repo.getSpaceA2aSkills(spaceId);
        if (allowlisted.length === 0) continue;
        grantedSpaceIds.push(spaceId);
        for (const skill of allowlisted) {
          if (requestedSkillSet.has(skill)) grantedSkillsSet.add(skill);
        }
      }

      // Grant を作って scope/resource を付与。
      // a2a.read は oidc-config の top-level `scopes` に載る OP スコープでもあるため、
      // resource スコープだけでなく OP スコープとしても付与しないと、consent prompt の
      // `op_scopes_missing` が解消されず認可コードフローが consent ループに陥る。
      const GrantCtor = (provider as any).Grant;
      const grant = new GrantCtor({ accountId, clientId });
      grant.addOIDCScope('openid');
      grant.addOIDCScope(A2A_READ_SCOPE);
      grant.addResourceScope(resource, A2A_READ_SCOPE);
      const grantId: string = await grant.save();

      // 委任行を書き込む（grant.save() 直後、interactionResult より前）。
      // スコープ外のみ選択された場合は grantedSpaceIds が空になる → 孤立行を作らない。
      if (grantedSpaceIds.length > 0) {
        repo.createA2aDelegation({
          id: randomUUID(),
          userId: accountId,
          clientId,
          grantId,
          grantedSpaceIds,
          grantedSkills: [...grantedSkillsSet],
          audience: resource,
          expiresAt: null,
          revokedAt: null,
        });
      }

      const result = { login: { accountId }, consent: { grantId } };
      const redirectTo = await provider.interactionResult(req, res, result, { mergeWithLastSubmission: false });
      try {
        await repo.addAuditLog(null, 'a2a.consent.granted', accountId, { clientId, resource, grantId });
      } catch (auditErr) {
        logger.warn(`[a2a-oidc] audit log failed (non-fatal): ${auditErr}`);
      }
      res.json({ redirectTo });
    } catch (err) {
      logger.error(`[a2a-oidc] interaction confirm error: ${err}`);
      res.status(500).json({ error: 'confirm error' });
    }
  });

  router.post('/:uid/abort', async (req: Request, res: Response) => {
    try {
      const result = { error: 'access_denied', error_description: 'user aborted' };
      const redirectTo = await provider.interactionResult(req, res, result, { mergeWithLastSubmission: false });
      res.json({ redirectTo });
    } catch (err) {
      logger.error(`[a2a-oidc] interaction abort error: ${err}`);
      res.status(500).json({ error: 'abort error' });
    }
  });

  return router;
}

function escapeHtml(s: string): string {
  return s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!));
}

type SpaceOption = { id: string; title: string; skills: string[] };

function renderConsent(uid: string, clientName: string, scopes: string[], spaces: SpaceOption[]): string {
  const scopeItems = scopes.map(s => `<li>${escapeHtml(s)}</li>`).join('');
  const spaceSection = spaces.length === 0
    ? '<p><em>委任可能なスペースがありません。</em></p>'
    : spaces.map(sp => {
        const skillBoxes = sp.skills.map(sk =>
          `<label style="display:block;margin-left:16px"><input type="checkbox" name="selectedSkills" value="${escapeHtml(sk)}" checked> ${escapeHtml(sk)}</label>`
        ).join('');
        return `<div style="margin:8px 0;padding:8px;border:1px solid #ccc;border-radius:4px"><label><input type="checkbox" name="selectedSpaces" value="${escapeHtml(sp.id)}" checked> <strong>${escapeHtml(sp.title)}</strong></label>${skillBoxes}</div>`;
      }).join('');
  return `<!doctype html><meta charset="utf-8"><title>A2A 委任の同意</title>
<body style="font-family:sans-serif;max-width:480px;margin:40px auto">
<h1>外部エージェントへの委任</h1>
<p><strong>${escapeHtml(clientName)}</strong> があなたの代理として次の権限を要求しています。</p>
<ul>${scopeItems}</ul>
<form method="post" action="/oidc/interaction/${encodeURIComponent(uid)}/confirm">
  <h2>委任するスペースとスキル</h2>
  ${spaceSection}
  <button type="submit">許可する</button>
</form>
<form method="post" action="/oidc/interaction/${encodeURIComponent(uid)}/abort" style="margin-top:8px">
  <button type="submit">拒否する</button>
</form>
</body>`;
}
