import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

const GITEA_ORG = 'FFAIApps';

/**
 * Gitea API 错误码（GiteaClientService 抛出 / 返回）
 *
 * Phase 0 调用方据此区分"运维待提供"vs"业务可重试"。
 */
export type GiteaError =
  | { code: 'gitea_token_missing'; message: string }
  | { code: 'gitea_token_insufficient_scope'; message: string; required?: string[] }
  | { code: 'gitea_org_not_found'; message: string; org: string }
  | { code: 'gitea_repo_already_exists'; message: string; fullName: string }
  | { code: 'gitea_unreachable'; message: string; cause?: string }
  | { code: 'gitea_webhook_install_failed'; message: string; fullName: string; cause?: string }
  | { code: 'gitea_webhook_base_url_missing'; message: string }
  | { code: 'gitea_unknown'; message: string; status?: number; body?: string };

export interface GiteaRepoSummary {
  id: number;
  fullName: string; // 'FFAIApps/zhang-san-birthday-reminder'
  cloneUrl: string; // 'http://.../FFAIApps/zhang-san-birthday-reminder.git'
  sshUrl: string;
}

export interface PushCredential {
  /**
   * Phase 0 实现：返回平台 API token 明文（同 INTERNAL_APP_GITEA_API_TOKEN）
   * 真实 5min TTL fine-grained token 需 Gitea 管理员账号 + `write:user` scope，留待 V2 升级。
   */
  token: string;
  expiresAt: Date;
  /** Phase 0 标记：当前 token 是平台长期 token，不是 5min ephemeral */
  isEphemeral: false;
}

/**
 * Gitea REST API 客户端
 *
 * 当前部署：`http://43.130.59.228`（内网，员工 push 需 VPN，详见 PRD §访问网络模型 §B）
 *
 * **运维提供物**：env `INTERNAL_APP_GITEA_API_TOKEN`
 *   - 必须 scope：`write:organization`（建仓 + 装 webhook） + `write:repository`（管理仓库设置）
 *   - 推荐 scope：`write:user`（V2 颁发 ephemeral fine-grained token）
 *   - 主 GITEA_API_TOKEN 通常 scope 不够，需要单独申请
 *
 * MVP 实现层级（PRD §核心约束 + 07-api §3.2）：
 * 1. ✅ createRepo: 在 FFAIApps org 下建仓
 * 2. ✅ getRepo: 探仓库是否存在（增量部署判断）
 * 3. 🟡 issuePushCredential: Phase 0 返回平台 token；V2 改 fine-grained ephemeral
 * 4. ⏳ installPreReceiveHook: 留 Phase 0 中期（PRD §部署 staging Layer 3）
 * 5. ⏳ archiveRepo: 销毁时调用，留 Phase 0 中后期
 *
 * 详见 docs/modules/internal-app-platform/03-architecture.md §3, §6
 */
@Injectable()
export class GiteaClientService {
  private readonly logger = new Logger(GiteaClientService.name);
  private readonly baseUrl: string;
  private readonly apiToken: string | undefined;

  constructor(private readonly config: ConfigService) {
    this.baseUrl =
      this.config.get<string>('INTERNAL_APP_GITEA_BASE_URL') ??
      'http://43.130.59.228';
    this.apiToken = this.config.get<string>('INTERNAL_APP_GITEA_API_TOKEN');
    if (!this.apiToken) {
      this.logger.warn(
        'INTERNAL_APP_GITEA_API_TOKEN 未配置，所有 Gitea 操作将立即返回 gitea_token_missing 错误',
      );
    }
  }

  /**
   * 在 FFAIApps org 下建仓（PRD §首次部署 = 建仓 + push）
   * 返回结构化结果或 GiteaError。
   */
  async createRepo(params: {
    employeeSlug: string;
    appSlug: string;
  }): Promise<{ ok: true; repo: GiteaRepoSummary } | { ok: false; error: GiteaError }> {
    if (!this.apiToken) {
      return {
        ok: false,
        error: {
          code: 'gitea_token_missing',
          message: 'INTERNAL_APP_GITEA_API_TOKEN 未配置',
        },
      };
    }

    const repoName = `${params.employeeSlug}-${params.appSlug}`;
    const url = `${this.baseUrl}/api/v1/orgs/${GITEA_ORG}/repos`;
    const body = {
      name: repoName,
      description: `Internal app: ${params.employeeSlug}/${params.appSlug}`,
      private: false, // org-internal visibility（V2 可调到 internal）
      auto_init: false, // 员工本地 push 才有首 commit
      default_branch: 'main',
    };

    let resp: Response;
    try {
      resp = await fetch(url, {
        method: 'POST',
        headers: {
          Authorization: `token ${this.apiToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
      });
    } catch (e) {
      const cause = e instanceof Error ? e.message : String(e);
      this.logger.error(`Gitea unreachable: ${cause}`);
      return {
        ok: false,
        error: { code: 'gitea_unreachable', message: 'Gitea API 不可达', cause },
      };
    }

    if (resp.status === 201) {
      const data = (await resp.json()) as {
        id: number;
        full_name: string;
        clone_url: string;
        ssh_url: string;
      };
      this.logger.log(`gitea repo created: ${data.full_name}`);
      return {
        ok: true,
        repo: {
          id: data.id,
          fullName: data.full_name,
          cloneUrl: data.clone_url,
          sshUrl: data.ssh_url,
        },
      };
    }

    return { ok: false, error: await this.parseError(resp, { org: GITEA_ORG, fullName: `${GITEA_ORG}/${repoName}` }) };
  }

  /**
   * 探仓库是否存在（增量部署判断）
   */
  async getRepo(params: {
    employeeSlug: string;
    appSlug: string;
  }): Promise<{ ok: true; exists: boolean; repo?: GiteaRepoSummary } | { ok: false; error: GiteaError }> {
    if (!this.apiToken) {
      return {
        ok: false,
        error: {
          code: 'gitea_token_missing',
          message: 'INTERNAL_APP_GITEA_API_TOKEN 未配置',
        },
      };
    }

    const repoName = `${params.employeeSlug}-${params.appSlug}`;
    const url = `${this.baseUrl}/api/v1/repos/${GITEA_ORG}/${repoName}`;

    let resp: Response;
    try {
      resp = await fetch(url, {
        method: 'GET',
        headers: { Authorization: `token ${this.apiToken}` },
      });
    } catch (e) {
      const cause = e instanceof Error ? e.message : String(e);
      return {
        ok: false,
        error: { code: 'gitea_unreachable', message: 'Gitea API 不可达', cause },
      };
    }

    if (resp.status === 200) {
      const data = (await resp.json()) as {
        id: number;
        full_name: string;
        clone_url: string;
        ssh_url: string;
      };
      return {
        ok: true,
        exists: true,
        repo: {
          id: data.id,
          fullName: data.full_name,
          cloneUrl: data.clone_url,
          sshUrl: data.ssh_url,
        },
      };
    }
    if (resp.status === 404) {
      return { ok: true, exists: false };
    }
    return { ok: false, error: await this.parseError(resp, { org: GITEA_ORG, fullName: `${GITEA_ORG}/${repoName}` }) };
  }

  /**
   * 颁发 git push 凭据
   *
   * Phase 0 MVP：直接返回平台 INTERNAL_APP_GITEA_API_TOKEN（长期 token），
   * `expiresAt = now + 5 分钟`（仅作客户端语义提示，server 端不撤销）。
   *
   * V2 升级：调 Gitea `POST /api/v1/users/{username}/tokens` 颁发真 5min TTL
   *   fine-grained token，scope = `write:repository:{owner/name}`；
   *   到期 Gitea server 自动失效。需要 `write:user` scope 主 token。
   */
  issuePushCredential(): { ok: true; credential: PushCredential } | { ok: false; error: GiteaError } {
    if (!this.apiToken) {
      return {
        ok: false,
        error: {
          code: 'gitea_token_missing',
          message: 'INTERNAL_APP_GITEA_API_TOKEN 未配置，无法颁发 push 凭据',
        },
      };
    }
    return {
      ok: true,
      credential: {
        token: this.apiToken,
        expiresAt: new Date(Date.now() + 5 * 60 * 1000),
        isEphemeral: false,
      },
    };
  }

  /**
   * 确保仓库装有指向本环境 backend 的 push webhook（幂等）。
   *
   * 历史 bug：createRepo 从未装 per-repo webhook，依赖运维一次性配的 org-level webhook。
   * 当 org-level URL 漂到死 IP / 被改 / 被删时，所有"无 per-repo 兜底"的仓库 push 完静默
   * 丢消息，员工拿到 200 空 body 死胡同（详见 .learnings/2026-05-19-gitea-webhook-...）。
   *
   * 修复：deploy_prepare 每次必须确保该仓库有一条 events=[push] 且 URL 匹配本环境
   * 期望值的 webhook。已有则跳过，没有则装。idempotent 安全多次调用。
   *
   * 期望 URL = `${INTERNAL_APP_WEBHOOK_BASE_URL || FRONTEND_URL}/api/v1/internal-apps/webhook/gitea`
   */
  async ensureWebhook(
    repoFullName: string,
  ): Promise<{ ok: true; created: boolean } | { ok: false; error: GiteaError }> {
    if (!this.apiToken) {
      return {
        ok: false,
        error: { code: 'gitea_token_missing', message: 'INTERNAL_APP_GITEA_API_TOKEN 未配置' },
      };
    }
    const baseUrl =
      this.config.get<string>('INTERNAL_APP_WEBHOOK_BASE_URL') ??
      this.config.get<string>('FRONTEND_URL');
    if (!baseUrl) {
      return {
        ok: false,
        error: {
          code: 'gitea_webhook_base_url_missing',
          message: 'INTERNAL_APP_WEBHOOK_BASE_URL（或 FRONTEND_URL 兜底）未配置，无法装 webhook',
        },
      };
    }
    const expectedUrl = `${baseUrl.replace(/\/$/, '')}/api/v1/internal-apps/webhook/gitea`;
    const secret = this.config.get<string>('INTERNAL_APP_GITEA_WEBHOOK_SECRET') ?? '';
    if (!secret) {
      this.logger.warn(
        'INTERNAL_APP_GITEA_WEBHOOK_SECRET 未配置，新建的 webhook 无 HMAC secret，签名校验会失败',
      );
    }

    const hooksUrl = `${this.baseUrl}/api/v1/repos/${repoFullName}/hooks`;
    let listResp: Response;
    try {
      listResp = await fetch(hooksUrl, {
        method: 'GET',
        headers: { Authorization: `token ${this.apiToken}` },
      });
    } catch (e) {
      return {
        ok: false,
        error: {
          code: 'gitea_unreachable',
          message: 'Gitea API 不可达',
          cause: e instanceof Error ? e.message : String(e),
        },
      };
    }
    if (!listResp.ok) {
      return {
        ok: false,
        error: await this.parseError(listResp, { org: GITEA_ORG, fullName: repoFullName }),
      };
    }
    const existing = (await listResp.json()) as Array<{
      id: number;
      config: { url?: string };
      events: string[];
      active: boolean;
    }>;
    const match = existing.find(
      (h) => h.config?.url === expectedUrl && h.active && h.events?.includes('push'),
    );
    if (match) {
      return { ok: true, created: false };
    }

    // 创建
    const body = {
      type: 'gitea',
      active: true,
      events: ['push'],
      config: {
        url: expectedUrl,
        content_type: 'json',
        secret,
      },
    };
    let createResp: Response;
    try {
      createResp = await fetch(hooksUrl, {
        method: 'POST',
        headers: {
          Authorization: `token ${this.apiToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
      });
    } catch (e) {
      return {
        ok: false,
        error: {
          code: 'gitea_webhook_install_failed',
          message: `装 webhook 时 Gitea 不可达 (${repoFullName})`,
          fullName: repoFullName,
          cause: e instanceof Error ? e.message : String(e),
        },
      };
    }
    if (createResp.status !== 201) {
      const errBody = await createResp.text().catch(() => '');
      return {
        ok: false,
        error: {
          code: 'gitea_webhook_install_failed',
          message: `装 webhook 失败 (${repoFullName}) status=${createResp.status}`,
          fullName: repoFullName,
          cause: errBody.slice(0, 400),
        },
      };
    }
    this.logger.log(`webhook installed: ${repoFullName} → ${expectedUrl}`);
    return { ok: true, created: true };
  }

  /**
   * 归档 Gitea 仓库（destroy 调用，PRD F2.4 + 30 天保留）
   *
   * Gitea 归档 = repo 设为 read-only：禁止 push / merge / issue 创建，但 clone/查阅照常。
   * 30 天保留期内 IT-Admin 可恢复（PATCH archived=false）或彻底删（DELETE repo）。
   *
   * 幂等：仓库不存在 / 已归档 都视为成功（destroy 操作允许"未真建过 Gitea 仓"）。
   */
  async archiveRepo(params: {
    employeeSlug: string;
    appSlug: string;
  }): Promise<{ ok: true; alreadyArchived: boolean } | { ok: false; error: GiteaError }> {
    if (!this.apiToken) {
      return {
        ok: false,
        error: {
          code: 'gitea_token_missing',
          message: 'INTERNAL_APP_GITEA_API_TOKEN 未配置，无法归档仓库',
        },
      };
    }
    const repoName = `${params.employeeSlug}-${params.appSlug}`;
    const fullName = `${GITEA_ORG}/${repoName}`;
    const url = `${this.baseUrl}/api/v1/repos/${fullName}`;

    let resp: Response;
    try {
      resp = await fetch(url, {
        method: 'PATCH',
        headers: {
          Authorization: `token ${this.apiToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ archived: true }),
      });
    } catch (e) {
      const cause = e instanceof Error ? e.message : String(e);
      return {
        ok: false,
        error: { code: 'gitea_unreachable', message: 'Gitea API 不可达', cause },
      };
    }

    if (resp.status === 200) {
      const data = (await resp.json().catch(() => ({}))) as { archived?: boolean };
      this.logger.log(`gitea repo archived: ${fullName}`);
      return { ok: true, alreadyArchived: data.archived === true };
    }
    if (resp.status === 404) {
      // 仓库不存在 = 销毁场景下视为成功（PoC 路径里可能 deploy_prepare 没建仓就 destroy）
      this.logger.warn(`archiveRepo: repo ${fullName} 不存在，幂等视为成功`);
      return { ok: true, alreadyArchived: false };
    }
    return { ok: false, error: await this.parseError(resp, { org: GITEA_ORG, fullName }) };
  }

  /** Gitea 错误响应解析（识别 token scope / 404 / 409 / 5xx） */
  private async parseError(
    resp: Response,
    ctx: { org: string; fullName: string },
  ): Promise<GiteaError> {
    let body = '';
    try {
      body = await resp.text();
    } catch {
      /* ignore */
    }
    const lowerBody = body.toLowerCase();

    if (resp.status === 403 && lowerBody.includes('required scope')) {
      // Gitea 返回: "token does not have at least one of required scope(s), required=[...]"
      const m = /required=\[([^\]]+)\]/.exec(body);
      const required = m ? m[1].split(',').map((s) => s.trim()) : undefined;
      return {
        code: 'gitea_token_insufficient_scope',
        message: `INTERNAL_APP_GITEA_API_TOKEN 缺 scope${required ? `：${required.join(', ')}` : ''}`,
        required,
      };
    }
    if (resp.status === 404) {
      return {
        code: 'gitea_org_not_found',
        message: `Gitea org "${ctx.org}" 不存在或当前 token 无权访问`,
        org: ctx.org,
      };
    }
    if (resp.status === 409) {
      return {
        code: 'gitea_repo_already_exists',
        message: `仓库 "${ctx.fullName}" 已存在`,
        fullName: ctx.fullName,
      };
    }
    return {
      code: 'gitea_unknown',
      message: `Gitea API 返回 ${resp.status}`,
      status: resp.status,
      body: body.slice(0, 500),
    };
  }
}
