import { env } from '../../config/env';
import { prisma } from '../../config/database';
import { logger } from '../../utils/logger';
import { encrypt } from '../../utils/crypto';
import { giteaClient } from '../../services/gitea.client';
import { updateEnvFile } from '../../services/env-writer.service';

const SETUP_TIMEOUT = 15000;

interface StepResult {
  step: string;
  success: boolean;
  error?: string;
  data?: Record<string, unknown>;
}

interface SetupResult {
  success: boolean;
  steps: StepResult[];
  error?: string;
}

/**
 * Make a Gitea API request with Basic Auth (for bootstrapping before we have a token).
 */
async function giteaBasicRequest<T>(
  method: string,
  path: string,
  username: string,
  password: string,
  body?: Record<string, unknown>,
): Promise<T> {
  const url = `${env.GITEA_URL}/api/v1${path}`;
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), SETUP_TIMEOUT);

  const headers: Record<string, string> = {
    Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
  };

  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 ${method} ${path}: ${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);
  }
}

/**
 * Make a Gitea API request with token auth.
 */
async function giteaTokenRequest<T>(
  method: string,
  path: string,
  token: string,
  body?: Record<string, unknown>,
): Promise<T> {
  const url = `${env.GITEA_URL}/api/v1${path}`;
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), SETUP_TIMEOUT);

  const headers: Record<string, string> = {
    Authorization: `token ${token}`,
  };

  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 ${method} ${path}: ${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);
  }
}

/**
 * Check if Gitea is reachable and the install page has been completed.
 */
async function checkStatus(): Promise<{
  giteaOnline: boolean;
  installComplete: boolean;
  tokenConfigured: boolean;
  reposCreated: boolean;
  oauthConfigured: boolean;
  setupComplete: boolean;
}> {
  let giteaOnline = false;
  let installComplete = false;
  let tokenConfigured = false;
  let reposCreated = false;
  let oauthConfigured = false;

  // Check Gitea is reachable via the public healthz endpoint.
  // `/api/healthz` is exempt from REQUIRE_SIGNIN_VIEW, which gates every other /api/v1/* route.
  // Using an auth-gated endpoint here would produce a false-negative 403 on a perfectly healthy Gitea.
  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5000);
    try {
      const res = await fetch(`${env.GITEA_URL}/api/healthz`, {
        signal: controller.signal,
        redirect: 'manual',
      });
      giteaOnline = res.ok;
      if (!res.ok) {
        logger.debug(`Gitea /api/healthz returned ${res.status} ${res.statusText}`);
      }
    } finally {
      clearTimeout(timeout);
    }
  } catch (err) {
    logger.debug(`Gitea /api/healthz unreachable: ${err instanceof Error ? err.message : String(err)}`);
  }

  // Check DB settings
  let dbSetupComplete = false;
  try {
    const settings = await prisma.siteSettings.findFirst({
      select: {
        giteaApiToken: true,
        giteaOauthClientId: true,
        giteaOauthClientSecret: true,
        giteaSetupComplete: true,
      },
    });

    if (settings) {
      tokenConfigured = !!settings.giteaApiToken;
      oauthConfigured = !!settings.giteaOauthClientId && !!settings.giteaOauthClientSecret;
      dbSetupComplete = !!settings.giteaSetupComplete;
    }
  } catch {
    // DB not available
  }

  // `installComplete` here means: Gitea's install wizard is done AND an admin user exists.
  // `giteaOnline` alone (healthz 200) proves the process is up but says nothing about admin bootstrap.
  // Preference order:
  //   1. If DB flag is set, trust it (fast path — no network call)
  //   2. Else if Gitea is online and we have an admin password, probe /user with Basic Auth
  if (dbSetupComplete) {
    installComplete = true;
  } else if (giteaOnline && env.GITEA_ADMIN_PASSWORD) {
    try {
      await giteaBasicRequest<{ login: string }>('GET', '/user', 'admin', env.GITEA_ADMIN_PASSWORD);
      installComplete = true;
    } catch {
      // Admin user doesn't exist or password mismatch — installComplete stays false
    }
  }

  if (dbSetupComplete) {
    return {
      giteaOnline,
      installComplete,
      tokenConfigured,
      reposCreated: true, // Trust the flag
      oauthConfigured,
      setupComplete: true,
    };
  }

  // Check repos if we have a token
  if (tokenConfigured && giteaOnline) {
    try {
      const config = await giteaClient.getConfig();
      if (config.apiToken) {
        await giteaTokenRequest('GET', `/repos/admin/docs-comments`, config.apiToken);
        await giteaTokenRequest('GET', `/repos/admin/changemaker.lite`, config.apiToken);
        reposCreated = true;
      }
    } catch {
      // Repos don't exist
    }
  }

  return {
    giteaOnline,
    installComplete,
    tokenConfigured,
    reposCreated,
    oauthConfigured,
    setupComplete: tokenConfigured && reposCreated,
  };
}

/**
 * Test connection with basic auth credentials.
 */
async function testConnection(username: string, password: string): Promise<{
  success: boolean;
  giteaVersion?: string;
  error?: string;
}> {
  try {
    const data = await giteaBasicRequest<{ version: string }>(
      'GET', '/version', username, password,
    );
    // Also verify admin access
    await giteaBasicRequest<{ login: string; is_admin: boolean }>(
      'GET', '/user', username, password,
    );
    return { success: true, giteaVersion: data.version };
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    if (msg.includes('401')) {
      return { success: false, error: 'Invalid username or password' };
    }
    return { success: false, error: msg };
  }
}

/**
 * Run the full Gitea setup: create token, repos, labels, OAuth app.
 */
async function runFullSetup(username: string, password: string): Promise<SetupResult> {
  const steps: StepResult[] = [];

  // Step 1: Check Gitea is reachable
  try {
    await giteaBasicRequest<{ version: string }>('GET', '/version', username, password);
    steps.push({ step: 'check_connection', success: true });
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    steps.push({ step: 'check_connection', success: false, error: msg });
    return { success: false, steps, error: 'Cannot connect to Gitea' };
  }

  // Step 2: Create API token
  let apiToken = '';
  try {
    // First try to delete any existing token with the same name
    try {
      await giteaBasicRequest('DELETE', `/users/${username}/tokens/changemaker-auto`, username, password);
    } catch {
      // Token doesn't exist, that's fine
    }

    const tokenData = await giteaBasicRequest<{ sha1: string }>(
      'POST',
      `/users/${username}/tokens`,
      username,
      password,
      { name: 'changemaker-auto', scopes: ['all'] },
    );
    apiToken = tokenData.sha1;
    steps.push({ step: 'create_token', success: true, data: { tokenName: 'changemaker-auto' } });
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    steps.push({ step: 'create_token', success: false, error: msg });
    return { success: false, steps, error: 'Failed to create API token' };
  }

  // Step 3: Create docs-comments repo
  try {
    try {
      await giteaTokenRequest('GET', `/repos/${username}/docs-comments`, apiToken);
      steps.push({ step: 'create_comments_repo', success: true, data: { note: 'Already exists' } });
    } catch {
      await giteaTokenRequest('POST', '/user/repos', apiToken, {
        name: 'docs-comments',
        description: 'Documentation page comments — managed by Changemaker Lite',
        private: false,
        auto_init: true,
      });
      steps.push({ step: 'create_comments_repo', success: true });
    }
  } catch (err) {
    steps.push({ step: 'create_comments_repo', success: false, error: err instanceof Error ? err.message : String(err) });
  }

  // Step 4: Create changemaker.lite repo (for version history)
  try {
    try {
      await giteaTokenRequest('GET', `/repos/${username}/changemaker.lite`, apiToken);
      steps.push({ step: 'create_history_repo', success: true, data: { note: 'Already exists' } });
    } catch {
      await giteaTokenRequest('POST', '/user/repos', apiToken, {
        name: 'changemaker.lite',
        description: 'Documentation version history — managed by Changemaker Lite',
        private: true,
        auto_init: true,
        default_branch: 'v2',
      });
      steps.push({ step: 'create_history_repo', success: true });
    }
  } catch (err) {
    steps.push({ step: 'create_history_repo', success: false, error: err instanceof Error ? err.message : String(err) });
  }

  // Step 5: Create labels on docs-comments repo
  try {
    const labels = [
      { name: 'docs-page', color: '#0075ca' },
      { name: 'anonymous', color: '#e4e669' },
      { name: 'moderated', color: '#0e8a16' },
    ];
    for (const label of labels) {
      try {
        await giteaTokenRequest('POST', `/repos/${username}/docs-comments/labels`, apiToken, label);
      } catch {
        // Label may already exist
      }
    }
    steps.push({ step: 'create_labels', success: true });
  } catch (err) {
    steps.push({ step: 'create_labels', success: false, error: err instanceof Error ? err.message : String(err) });
  }

  // Step 6: Create OAuth2 app
  let oauthClientId = '';
  let oauthClientSecret = '';
  try {
    // Check if OAuth app already exists
    const existingApps = await giteaTokenRequest<Array<{ id: number; name: string; client_id: string; client_secret: string }>>(
      'GET', '/user/applications/oauth2', apiToken,
    );
    const existing = existingApps.find(a => a.name === 'changemaker-docs');

    if (existing) {
      oauthClientId = existing.client_id;
      // Can't retrieve secret for existing app; leave blank if already set in DB
      steps.push({ step: 'create_oauth_app', success: true, data: { note: 'Already exists', clientId: oauthClientId } });
    } else {
      const domain = env.DOMAIN || 'cmlite.org';
      const oauthData = await giteaTokenRequest<{ client_id: string; client_secret: string }>(
        'POST', '/user/applications/oauth2', apiToken,
        {
          name: 'changemaker-docs',
          redirect_uris: [
            `https://${domain}/comments/callback/`,
            `https://docs.${domain}/comments/callback/`,
            `http://localhost:${env.MKDOCS_PORT || 4003}/comments/callback/`,
          ],
        },
      );
      oauthClientId = oauthData.client_id;
      oauthClientSecret = oauthData.client_secret;
      steps.push({ step: 'create_oauth_app', success: true, data: { clientId: oauthClientId } });
    }
  } catch (err) {
    steps.push({ step: 'create_oauth_app', success: false, error: err instanceof Error ? err.message : String(err) });
  }

  // Step 7: Save to database
  try {
    const updateData: Record<string, unknown> = {
      enableDocsComments: true,
      giteaApiToken: encrypt(apiToken),
      giteaCommentsRepoOwner: username,
      giteaCommentsRepoName: 'docs-comments',
      giteaSetupComplete: true,
    };
    if (oauthClientId) updateData.giteaOauthClientId = oauthClientId;
    if (oauthClientSecret) updateData.giteaOauthClientSecret = encrypt(oauthClientSecret);

    await prisma.siteSettings.updateMany({ data: updateData });

    // Clear config cache so the running system picks up new credentials
    giteaClient.clearConfigCache();

    steps.push({ step: 'save_config', success: true });
  } catch (err) {
    steps.push({ step: 'save_config', success: false, error: err instanceof Error ? err.message : String(err) });
    return { success: false, steps, error: 'Failed to save configuration' };
  }

  const allStepsSucceeded = steps.every(s => s.success);
  return { success: allStepsSucceeded, steps };
}

/**
 * Auto-setup if GITEA_ADMIN_PASSWORD is set and setup hasn't been completed.
 * Called on API startup. Fire-and-forget with retries.
 */
async function autoSetupIfNeeded(): Promise<{ alreadyComplete: boolean; success: boolean; error?: string }> {
  const password = env.GITEA_ADMIN_PASSWORD;
  if (!password) return { alreadyComplete: true, success: true };

  // Check if already complete
  try {
    const settings = await prisma.siteSettings.findFirst({
      select: { giteaSetupComplete: true },
    });
    if (settings?.giteaSetupComplete) {
      return { alreadyComplete: true, success: true };
    }
  } catch {
    // DB might not be ready yet
  }

  // Wait for Gitea to be available and admin user to exist (up to 8 retries, 10s apart).
  // The gitea-init container creates the admin user after gitea-app is healthy,
  // so we need to wait for both Gitea web AND admin auth to be ready.
  // Note: REQUIRE_SIGNIN_VIEW=true blocks unauthenticated /version, so we use
  // Basic Auth for the readiness check directly.
  let giteaReady = false;
  for (let i = 0; i < 8; i++) {
    try {
      // Check if Gitea is online AND admin auth works in one shot
      await giteaBasicRequest<{ login: string }>('GET', '/user', 'admin', password);
      giteaReady = true;
      break;
    } catch {
      // Not ready yet — Gitea may not be up, or admin user may not exist yet
    }
    if (i < 7) {
      logger.info(`Gitea auto-setup: waiting for Gitea + admin user (attempt ${i + 1}/8)...`);
      await new Promise(r => setTimeout(r, 10000));
    }
  }

  if (!giteaReady) {
    return { alreadyComplete: false, success: false, error: 'Gitea not reachable or admin user not ready after 8 attempts' };
  }

  // Run setup
  logger.info('Gitea auto-setup: running initial setup...');
  const result = await runFullSetup('admin', password);

  if (result.success) {
    logger.info('Gitea auto-setup: completed successfully');
    for (const step of result.steps) {
      logger.info(`  ${step.step}: ${step.success ? 'OK' : 'FAILED'}${step.data?.note ? ` (${step.data.note})` : ''}`);
    }

    // Clear GITEA_ADMIN_PASSWORD from .env — it's no longer needed after setup
    // (the API token is now encrypted in the database)
    try {
      const envResult = updateEnvFile({ GITEA_ADMIN_PASSWORD: '' });
      if (envResult.success) {
        logger.info('Gitea auto-setup: cleared GITEA_ADMIN_PASSWORD from .env');
      }
    } catch (err) {
      logger.warn(`Gitea auto-setup: could not clear GITEA_ADMIN_PASSWORD from .env: ${err instanceof Error ? err.message : err}`);
    }
  } else {
    logger.warn(`Gitea auto-setup: failed — ${result.error}`);
    for (const step of result.steps) {
      if (!step.success) logger.warn(`  ${step.step}: ${step.error}`);
    }
  }

  return { alreadyComplete: false, success: result.success, error: result.error };
}

export const giteaSetupService = {
  checkStatus,
  testConnection,
  runFullSetup,
  autoSetupIfNeeded,
};
