import { env } from '../config/env';
import { logger } from '../utils/logger';
import { prisma } from '../config/database';
import { decrypt } from '../utils/crypto';

// --- Types ---

export interface GiteaIssue {
  id: number;
  number: number;
  title: string;
  body: string;
  state: string;
  labels: GiteaLabel[];
  created_at: string;
  updated_at: string;
}

export interface GiteaComment {
  id: number;
  body: string;
  user: GiteaUser;
  created_at: string;
  updated_at: string;
}

export interface GiteaUser {
  id: number;
  login: string;
  full_name: string;
  avatar_url: string;
  email: string;
}

export interface GiteaLabel {
  id: number;
  name: string;
  color: string;
}

interface GiteaOAuthTokenResponse {
  access_token: string;
  token_type: string;
  refresh_token?: string;
  expires_in?: number;
}

interface GiteaRepo {
  id: number;
  name: string;
  full_name: string;
}

/** Resolved config from DB settings with env var fallback */
interface GiteaCommentsConfig {
  enabled: boolean;
  apiToken: string;
  repoOwner: string;
  repoName: string;
  oauthClientId: string;
  oauthClientSecret: string;
}

// --- Client ---

class GiteaClient {
  // Cache resolved config for 60s to avoid DB hit on every API call
  private configCache: GiteaCommentsConfig | null = null;
  private configCacheExpiry = 0;

  private get baseUrl(): string {
    return env.GITEA_URL;
  }

  /**
   * Load Gitea comments config from DB settings (with env var fallback).
   * Cached for 60 seconds in memory.
   */
  async getConfig(): Promise<GiteaCommentsConfig> {
    if (this.configCache && Date.now() < this.configCacheExpiry) {
      return this.configCache;
    }

    let dbEnabled = false;
    let dbApiToken = '';
    let dbRepoOwner = '';
    let dbRepoName = '';
    let dbOauthClientId = '';
    let dbOauthClientSecret = '';

    try {
      const settings = await prisma.siteSettings.findFirst({
        select: {
          enableDocsComments: true,
          giteaApiToken: true,
          giteaCommentsRepoOwner: true,
          giteaCommentsRepoName: true,
          giteaOauthClientId: true,
          giteaOauthClientSecret: true,
        },
      });

      if (settings) {
        dbEnabled = settings.enableDocsComments;
        dbApiToken = this.tryDecrypt(settings.giteaApiToken);
        dbRepoOwner = settings.giteaCommentsRepoOwner;
        dbRepoName = settings.giteaCommentsRepoName;
        dbOauthClientId = settings.giteaOauthClientId;
        dbOauthClientSecret = this.tryDecrypt(settings.giteaOauthClientSecret);
      }
    } catch (err) {
      logger.debug('Gitea config: DB lookup failed, using env fallback:', err instanceof Error ? err.message : err);
    }

    // DB settings take priority; env vars are fallback
    const config: GiteaCommentsConfig = {
      enabled: dbEnabled || env.GITEA_COMMENTS_ENABLED === 'true',
      apiToken: dbApiToken || env.GITEA_API_TOKEN,
      repoOwner: dbRepoOwner || env.GITEA_COMMENTS_REPO_OWNER,
      repoName: dbRepoName || env.GITEA_COMMENTS_REPO_NAME || 'docs-comments',
      oauthClientId: dbOauthClientId || env.GITEA_OAUTH_CLIENT_ID,
      oauthClientSecret: dbOauthClientSecret || env.GITEA_OAUTH_CLIENT_SECRET,
    };

    this.configCache = config;
    this.configCacheExpiry = Date.now() + 60_000;
    return config;
  }

  /** Invalidate the cached config (call after settings update) */
  clearConfigCache(): void {
    this.configCache = null;
    this.configCacheExpiry = 0;
  }

  /** Try to decrypt a value; if it's not encrypted (plain text), return as-is */
  private tryDecrypt(value: string): string {
    if (!value) return '';
    try {
      return decrypt(value);
    } catch {
      return value; // Not encrypted (e.g. during transition)
    }
  }

  /**
   * Make an authenticated request to Gitea API using the service account token
   */
  private async request<T>(
    method: string,
    path: string,
    body?: Record<string, unknown>,
    userToken?: string,
  ): Promise<T> {
    const config = await this.getConfig();
    const url = `${this.baseUrl}/api/v1${path}`;
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 15000);

    const headers: Record<string, string> = {
      Authorization: `token ${userToken || config.apiToken}`,
    };

    let fetchBody: string | undefined;
    if (body) {
      headers['Content-Type'] = 'application/json';
      fetchBody = JSON.stringify(body);
    }

    try {
      const res = await fetch(url, {
        method,
        headers,
        body: fetchBody,
        signal: controller.signal,
      });

      if (!res.ok) {
        const text = await res.text().catch(() => '');
        throw new Error(`Gitea API ${method} ${path} returned ${res.status}: ${text}`);
      }

      const contentType = res.headers.get('content-type') || '';
      if (contentType.includes('application/json')) {
        return (await res.json()) as T;
      }
      return {} as T;
    } finally {
      clearTimeout(timeout);
    }
  }

  // --- Health ---

  async isAvailable(): Promise<boolean> {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 5000);
      try {
        const res = await fetch(`${this.baseUrl}/api/v1/version`, {
          signal: controller.signal,
        });
        return res.ok;
      } finally {
        clearTimeout(timeout);
      }
    } catch {
      return false;
    }
  }

  // --- Config-aware helpers (async) ---

  async isEnabled(): Promise<boolean> {
    const config = await this.getConfig();
    return config.enabled && !!config.apiToken && !!config.repoOwner;
  }

  async isOAuthEnabled(): Promise<boolean> {
    const config = await this.getConfig();
    return config.enabled && !!config.apiToken && !!config.repoOwner
      && !!config.oauthClientId && !!config.oauthClientSecret;
  }

  private async getRepoPath(): Promise<string> {
    const config = await this.getConfig();
    return `${config.repoOwner}/${config.repoName}`;
  }

  // --- Issue Management ---

  /**
   * Find an issue by exact title match in the comments repo
   */
  async findIssueByTitle(title: string): Promise<GiteaIssue | null> {
    if (!(await this.isEnabled())) return null;

    try {
      const repoPath = await this.getRepoPath();
      const issues = await this.request<GiteaIssue[]>(
        'GET',
        `/repos/${repoPath}/issues?type=issues&state=open&q=${encodeURIComponent(title)}&limit=10`,
      );

      // Filter for exact title match (search is fuzzy)
      return issues.find((i) => i.title === title) || null;
    } catch (err) {
      logger.warn('Gitea findIssueByTitle failed:', err instanceof Error ? err.message : err);
      return null;
    }
  }

  /**
   * Create a new issue for a docs page
   */
  async createIssue(title: string, body: string): Promise<GiteaIssue | null> {
    if (!(await this.isEnabled())) return null;

    try {
      const repoPath = await this.getRepoPath();
      const issue = await this.request<GiteaIssue>(
        'POST',
        `/repos/${repoPath}/issues`,
        { title, body },
      );

      logger.info(`Gitea: created issue #${issue.number} for "${title}"`);
      return issue;
    } catch (err) {
      logger.warn('Gitea createIssue failed:', err instanceof Error ? err.message : err);
      return null;
    }
  }

  /**
   * List all comments on an issue
   */
  async listIssueComments(issueNumber: number): Promise<GiteaComment[]> {
    if (!(await this.isEnabled())) return [];

    try {
      const repoPath = await this.getRepoPath();
      return await this.request<GiteaComment[]>(
        'GET',
        `/repos/${repoPath}/issues/${issueNumber}/comments?limit=50`,
      );
    } catch (err) {
      logger.warn(`Gitea listIssueComments(#${issueNumber}) failed:`, err instanceof Error ? err.message : err);
      return [];
    }
  }

  /**
   * Create a comment on an issue.
   * If userToken is provided, posts as that user. Otherwise uses service account.
   */
  async createIssueComment(
    issueNumber: number,
    body: string,
    userToken?: string,
  ): Promise<GiteaComment | null> {
    if (!(await this.isEnabled())) return null;

    try {
      const repoPath = await this.getRepoPath();
      const comment = await this.request<GiteaComment>(
        'POST',
        `/repos/${repoPath}/issues/${issueNumber}/comments`,
        { body },
        userToken,
      );

      return comment;
    } catch (err) {
      logger.warn(`Gitea createIssueComment(#${issueNumber}) failed:`, err instanceof Error ? err.message : err);
      return null;
    }
  }

  /**
   * Delete a comment from an issue (used for rejected anonymous comments)
   */
  async deleteIssueComment(commentId: number): Promise<boolean> {
    if (!(await this.isEnabled())) return false;

    try {
      const repoPath = await this.getRepoPath();
      await this.request(
        'DELETE',
        `/repos/${repoPath}/issues/comments/${commentId}`,
      );
      return true;
    } catch (err) {
      logger.warn(`Gitea deleteIssueComment(${commentId}) failed:`, err instanceof Error ? err.message : err);
      return false;
    }
  }

  // --- OAuth2 ---

  /**
   * Exchange an OAuth2 authorization code for an access token
   */
  async exchangeOAuthCode(code: string, redirectUri: string): Promise<GiteaOAuthTokenResponse | null> {
    if (!(await this.isOAuthEnabled())) return null;

    const config = await this.getConfig();
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 10000);

    try {
      const res = await fetch(`${this.baseUrl}/login/oauth/access_token`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
        body: JSON.stringify({
          client_id: config.oauthClientId,
          client_secret: config.oauthClientSecret,
          code,
          grant_type: 'authorization_code',
          redirect_uri: redirectUri,
        }),
        signal: controller.signal,
      });

      if (!res.ok) {
        const text = await res.text().catch(() => '');
        throw new Error(`OAuth token exchange failed (${res.status}): ${text}`);
      }

      return (await res.json()) as GiteaOAuthTokenResponse;
    } catch (err) {
      logger.warn('Gitea OAuth code exchange failed:', err instanceof Error ? err.message : err);
      return null;
    } finally {
      clearTimeout(timeout);
    }
  }

  /**
   * Get the authenticated user's profile using their OAuth token
   */
  async getAuthenticatedUser(userToken: string): Promise<GiteaUser | null> {
    try {
      return await this.request<GiteaUser>('GET', '/user', undefined, userToken);
    } catch (err) {
      logger.warn('Gitea getAuthenticatedUser failed:', err instanceof Error ? err.message : err);
      return null;
    }
  }

  // --- Admin Setup ---

  /**
   * Create the docs-comments repository if it doesn't exist
   */
  async createRepo(): Promise<GiteaRepo | null> {
    if (!(await this.isEnabled())) return null;

    try {
      const repoPath = await this.getRepoPath();
      // Check if repo already exists
      try {
        const existing = await this.request<GiteaRepo>(
          'GET',
          `/repos/${repoPath}`,
        );
        logger.info(`Gitea: repo ${existing.full_name} already exists`);
        return existing;
      } catch {
        // Repo doesn't exist, create it
      }

      const config = await this.getConfig();
      const repo = await this.request<GiteaRepo>('POST', `/user/repos`, {
        name: config.repoName,
        description: 'Documentation page comments — managed by Changemaker Lite',
        private: false,
        auto_init: true,
      });

      logger.info(`Gitea: created repo ${repo.full_name}`);
      return repo;
    } catch (err) {
      logger.warn('Gitea createRepo failed:', err instanceof Error ? err.message : err);
      return null;
    }
  }

  /**
   * Find or create a label in the comments repo
   */
  async findOrCreateLabel(name: string, color: string): Promise<GiteaLabel | null> {
    if (!(await this.isEnabled())) return null;

    try {
      const repoPath = await this.getRepoPath();
      const labels = await this.request<GiteaLabel[]>(
        'GET',
        `/repos/${repoPath}/labels`,
      );

      const existing = labels.find((l) => l.name === name);
      if (existing) return existing;

      return await this.request<GiteaLabel>(
        'POST',
        `/repos/${repoPath}/labels`,
        { name, color },
      );
    } catch (err) {
      logger.warn(`Gitea findOrCreateLabel("${name}") failed:`, err instanceof Error ? err.message : err);
      return null;
    }
  }

  /**
   * Get the OAuth2 authorize URL for the widget to redirect to
   */
  getAuthorizeUrl(): string {
    return `${this.baseUrl}/login/oauth/authorize`;
  }

  // --- Admin User Management (for user provisioning) ---

  /**
   * Create a new Gitea user via admin API
   */
  async adminCreateUser(data: {
    email: string;
    full_name: string;
    login_name: string;
    username: string;
    password: string;
    must_change_password?: boolean;
    send_notify?: boolean;
  }): Promise<GiteaUser> {
    return this.request<GiteaUser>('POST', '/admin/users', {
      email: data.email,
      full_name: data.full_name,
      login_name: data.login_name,
      username: data.username,
      password: data.password,
      must_change_password: data.must_change_password ?? false,
      send_notify: data.send_notify ?? false,
      visibility: 'limited',
    });
  }

  /**
   * Update an existing Gitea user via admin API
   */
  async adminUpdateUser(username: string, data: {
    email?: string;
    full_name?: string;
    login_name?: string;
    admin?: boolean;
    active?: boolean;
  }): Promise<GiteaUser> {
    return this.request<GiteaUser>('PATCH', `/admin/users/${encodeURIComponent(username)}`, data as Record<string, unknown>);
  }

  /**
   * Delete a Gitea user via admin API
   */
  async adminDeleteUser(username: string): Promise<void> {
    await this.request('DELETE', `/admin/users/${encodeURIComponent(username)}`);
  }

  /**
   * Find a Gitea user by email (searches all users)
   */
  async findUserByEmail(email: string): Promise<GiteaUser | null> {
    try {
      const users = await this.request<GiteaUser[]>(
        'GET',
        `/admin/users?limit=50`,
      );
      return users.find(u => u.email.toLowerCase() === email.toLowerCase()) || null;
    } catch (err) {
      logger.warn('Gitea findUserByEmail failed:', err instanceof Error ? err.message : err);
      return null;
    }
  }

  /**
   * Get a specific Gitea user by username
   */
  async getUserByUsername(username: string): Promise<GiteaUser | null> {
    try {
      return await this.request<GiteaUser>('GET', `/users/${encodeURIComponent(username)}`);
    } catch {
      return null;
    }
  }

  // --- Dashboard / Stats ---

  /**
   * List repositories (sorted by most recently updated)
   */
  async listRepos(limit = 50): Promise<Array<{
    id: number;
    name: string;
    full_name: string;
    description: string;
    updated_at: string;
    stars_count: number;
    forks_count: number;
    open_issues_count: number;
  }>> {
    try {
      return await this.request('GET', `/repos/search?limit=${limit}&sort=updated&order=desc`);
    } catch (err) {
      logger.warn('Gitea listRepos failed:', err instanceof Error ? err.message : err);
      return [];
    }
  }

  /**
   * List recent commits for a repository
   */
  async listRepoCommits(owner: string, repo: string, limit = 10): Promise<Array<{
    sha: string;
    commit: {
      message: string;
      author: { name: string; email: string; date: string };
    };
  }>> {
    try {
      return await this.request(
        'GET',
        `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits?limit=${limit}`,
      );
    } catch (err) {
      logger.warn(`Gitea listRepoCommits(${owner}/${repo}) failed:`, err instanceof Error ? err.message : err);
      return [];
    }
  }

  /**
   * List all users via admin API (for dashboard stats)
   */
  async listAllUsers(limit = 50): Promise<GiteaUser[]> {
    try {
      return await this.request<GiteaUser[]>('GET', `/admin/users?limit=${limit}`);
    } catch (err) {
      logger.warn('Gitea listAllUsers failed:', err instanceof Error ? err.message : err);
      return [];
    }
  }

  /**
   * Create an API token for a user (for SSO / iframe embedding)
   */
  async createUserToken(username: string, tokenName: string): Promise<{ id: number; name: string; sha1: string }> {
    return this.request<{ id: number; name: string; sha1: string }>(
      'POST',
      `/users/${encodeURIComponent(username)}/tokens`,
      { name: tokenName, scopes: ['read', 'write'] as unknown as Record<string, unknown> } as unknown as Record<string, unknown>,
    );
  }

  // --- Repository Collaborator Management ---

  /**
   * Add a collaborator to a repository with the specified permission level.
   * @param permission - "read", "write", or "admin"
   */
  async addCollaborator(
    owner: string,
    repo: string,
    username: string,
    permission: 'read' | 'write' | 'admin' = 'write',
  ): Promise<void> {
    await this.request(
      'PUT',
      `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
      { permission },
    );
  }

  /**
   * Remove a collaborator from a repository.
   */
  async removeCollaborator(owner: string, repo: string, username: string): Promise<void> {
    await this.request(
      'DELETE',
      `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
    );
  }

  /**
   * Check if a user is a collaborator on a repository.
   * Returns true if they are, false otherwise.
   */
  async isCollaborator(owner: string, repo: string, username: string): Promise<boolean> {
    try {
      await this.request(
        'GET',
        `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
      );
      return true;
    } catch {
      return false;
    }
  }
}

export const giteaClient = new GiteaClient();
