import { env } from '../../config/env';
import { logger } from '../../utils/logger';
import { giteaClient } from '../../services/gitea.client';

/**
 * Docs version history via Gitea API.
 * Auto-commits file saves and provides history/restore.
 *
 * Uses the main project repo (not the docs-comments repo).
 * Files are stored at mkdocs/docs/{path} in the repo.
 */

const GITEA_TIMEOUT = 15000;

/**
 * Encode a file path for Gitea API.
 * Gitea expects each path segment to be individually encoded.
 */
function encodeRepoPath(filePath: string): string {
  return filePath.split('/').map(encodeURIComponent).join('/');
}

interface GiteaCommit {
  sha: string;
  commit: {
    message: string;
    author: { name: string; email: string; date: string };
    committer: { name: string; email: string; date: string };
  };
}

interface GiteaFileContent {
  content: string; // base64 encoded
  sha: string;
  name: string;
  path: string;
}

function getBaseUrl(): string {
  return env.GITEA_URL;
}

async function getApiToken(): Promise<string> {
  // DB-first (token is stored encrypted in siteSettings after the setup wizard runs),
  // env var is only a bootstrap fallback for environments that set it manually.
  const config = await giteaClient.getConfig();
  return config.apiToken || '';
}

function getRepoPath(): string {
  // The main project repo — matches repo_url in mkdocs.yml
  return env.GITEA_DOCS_REPO || 'admin/changemaker.lite';
}

function getDocsPrefix(): string {
  // Relative path within repo where docs live
  return env.GITEA_DOCS_PREFIX || 'mkdocs/docs';
}

function getRepoBranch(): string {
  return env.GITEA_DOCS_BRANCH || 'v2';
}

/**
 * Make an authenticated Gitea API request.
 */
async function giteaRequest<T>(
  method: string,
  path: string,
  body?: Record<string, unknown>,
): Promise<T> {
  const token = await getApiToken();
  if (!token) throw new Error('Gitea API token not configured');

  const url = `${getBaseUrl()}/api/v1${path}`;
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), GITEA_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);
  }
}

/**
 * Get the current SHA of a file in the repo (needed for updates).
 */
async function getFileSha(repoFilePath: string): Promise<string | null> {
  try {
    const data = await giteaRequest<GiteaFileContent>(
      'GET',
      `/repos/${getRepoPath()}/contents/${encodeRepoPath(repoFilePath)}?ref=${getRepoBranch()}`,
    );
    return data.sha;
  } catch {
    return null;
  }
}

/**
 * Commit a file change to Gitea.
 * Fire-and-forget — errors are logged but don't propagate.
 */
async function commitFile(
  docsRelativePath: string,
  content: string,
  authorName: string,
  authorEmail: string,
): Promise<void> {
  const token = await getApiToken();
  if (!token) return; // Silently skip if not configured

  const repoFilePath = `${getDocsPrefix()}/${docsRelativePath}`;

  try {
    const existingSha = await getFileSha(repoFilePath);
    const base64Content = Buffer.from(content, 'utf-8').toString('base64');

    if (existingSha) {
      // Update existing file
      await giteaRequest(
        'PUT',
        `/repos/${getRepoPath()}/contents/${encodeRepoPath(repoFilePath)}`,
        {
          message: `Update ${docsRelativePath} by ${authorName}`,
          content: base64Content,
          sha: existingSha,
          branch: getRepoBranch(),
          author: { name: authorName, email: authorEmail },
        },
      );
    } else {
      // Create new file
      await giteaRequest(
        'POST',
        `/repos/${getRepoPath()}/contents/${encodeRepoPath(repoFilePath)}`,
        {
          message: `Create ${docsRelativePath} by ${authorName}`,
          content: base64Content,
          branch: getRepoBranch(),
          author: { name: authorName, email: authorEmail },
        },
      );
    }

    logger.debug(`Docs history: committed ${docsRelativePath} by ${authorName}`);
  } catch (err) {
    logger.warn(`Docs history: failed to commit ${docsRelativePath}:`, err instanceof Error ? err.message : err);
  }
}

/**
 * Get commit history for a documentation file.
 */
async function getFileHistory(docsRelativePath: string, limit = 20): Promise<GiteaCommit[]> {
  const repoFilePath = `${getDocsPrefix()}/${docsRelativePath}`;

  try {
    return await giteaRequest<GiteaCommit[]>(
      'GET',
      `/repos/${getRepoPath()}/commits?sha=${getRepoBranch()}&path=${encodeURIComponent(repoFilePath)}&limit=${limit}`,
    );
  } catch (err) {
    logger.warn(`Docs history: failed to get history for ${docsRelativePath}:`, err instanceof Error ? err.message : err);
    return [];
  }
}

/**
 * Get file content at a specific commit.
 */
async function getFileAtCommit(docsRelativePath: string, commitSha: string): Promise<string | null> {
  const repoFilePath = `${getDocsPrefix()}/${docsRelativePath}`;

  try {
    const data = await giteaRequest<GiteaFileContent>(
      'GET',
      `/repos/${getRepoPath()}/contents/${encodeRepoPath(repoFilePath)}?ref=${commitSha}`,
    );
    return Buffer.from(data.content, 'base64').toString('utf-8');
  } catch (err) {
    logger.warn(`Docs history: failed to get file at commit ${commitSha}:`, err instanceof Error ? err.message : err);
    return null;
  }
}

/**
 * Restore a file to a previous version by reading old content and creating a new commit.
 */
async function restoreRevision(
  docsRelativePath: string,
  commitSha: string,
  authorName: string,
  authorEmail: string,
): Promise<string | null> {
  const oldContent = await getFileAtCommit(docsRelativePath, commitSha);
  if (!oldContent) return null;

  const repoFilePath = `${getDocsPrefix()}/${docsRelativePath}`;
  const currentSha = await getFileSha(repoFilePath);
  if (!currentSha) return null;

  try {
    await giteaRequest(
      'PUT',
      `/repos/${getRepoPath()}/contents/${encodeRepoPath(repoFilePath)}`,
      {
        message: `Restore ${docsRelativePath} to revision ${commitSha.substring(0, 7)} by ${authorName}`,
        content: Buffer.from(oldContent, 'utf-8').toString('base64'),
        sha: currentSha,
        branch: getRepoBranch(),
        author: { name: authorName, email: authorEmail },
      },
    );
    return oldContent;
  } catch (err) {
    logger.warn(`Docs history: failed to restore ${docsRelativePath}:`, err instanceof Error ? err.message : err);
    return null;
  }
}

/**
 * Check if the Gitea history feature is available (token configured and Gitea reachable).
 */
async function isAvailable(): Promise<boolean> {
  const token = await getApiToken();
  if (!token) return false;

  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5000);
    try {
      // Use /api/healthz — public endpoint, exempt from REQUIRE_SIGNIN_VIEW.
      const res = await fetch(`${getBaseUrl()}/api/healthz`, {
        signal: controller.signal,
      });
      return res.ok;
    } finally {
      clearTimeout(timeout);
    }
  } catch {
    return false;
  }
}

export const docsHistoryService = {
  commitFile,
  getFileHistory,
  getFileAtCommit,
  restoreRevision,
  isAvailable,
};
