<?php

namespace App\Libraries;

use App\Models\GitCommitsModel;
use App\Models\GitPullRequestsModel;
use App\Models\GitRepositoriesModel;
use App\Models\GitUsersModel;

class GiteaSyncService
{
	private string $baseUrl;
	private string $token;
	private int $perPage = 100;

	private GitUsersModel $usersModel;
	private GitRepositoriesModel $repositoriesModel;
	private GitCommitsModel $commitsModel;
	private GitPullRequestsModel $pullRequestsModel;

	public function __construct()
	{
		$this->baseUrl = rtrim((string) (env('GITEA_BASE_URL') ?: 'https://gitea.services-summit.my.id/api/v1'), '/');
		$this->token = (string) env('GITEA_TOKEN');

		$this->usersModel = new GitUsersModel();
		$this->repositoriesModel = new GitRepositoriesModel();
		$this->commitsModel = new GitCommitsModel();
		$this->pullRequestsModel = new GitPullRequestsModel();
	}

	public function syncAll(): array
	{
		return $this->syncFull();
	}

	public function syncFull(): array
	{
		return $this->performSync();
	}

	public function syncIncremental(int $days = 1): array
	{
		if ($days < 1) {
			$days = 1;
		}

		$sinceIso = (new \DateTimeImmutable('now'))
			->modify('-' . $days . ' days')
			->format(DATE_ATOM);

		return $this->performSync($sinceIso, $days);
	}

	private function performSync(?string $sinceIso = null, ?int $days = null): array
	{
		if ($this->token === '') {
			return [
				'success' => false,
				'message' => 'GITEA_TOKEN missing in .env',
				'stats' => [],
			];
		}

		$stats = [
			'mode' => $sinceIso === null ? 'full' : 'incremental',
			'since' => $sinceIso,
			'users_synced' => 0,
			'repositories_synced' => 0,
			'commits_synced' => 0,
			'pull_requests_synced' => 0,
			'errors' => [],
		];

		$users = $this->fetchPaged('/users/search', [], ['data']);
		foreach ($users['items'] as $user) {
			$this->upsertUser($user);
			$stats['users_synced']++;
		}
		$stats['errors'] = array_merge($stats['errors'], $users['errors']);

		$repositories = $this->fetchPaged('/repos/search', [], ['data']);
		$stats['errors'] = array_merge($stats['errors'], $repositories['errors']);

		foreach ($repositories['items'] as $repo) {
			$localRepoId = $this->upsertRepository($repo);
			$stats['repositories_synced']++;

			if ($localRepoId === null) {
				$stats['errors'][] = 'Skip repo because upsert failed: ' . ($repo['full_name'] ?? 'unknown');
				continue;
			}

			$owner = $repo['owner']['username'] ?? $repo['owner']['login'] ?? null;
			$name = $repo['name'] ?? null;
			if ($owner === null || $name === null) {
				$stats['errors'][] = 'Skip repo because owner/name missing: ' . ($repo['full_name'] ?? 'unknown');
				continue;
			}

			$commitQuery = [];
			if ($sinceIso !== null) {
				$commitQuery['since'] = $sinceIso;
			}
			$commits = $this->fetchPaged("/repos/{$owner}/{$name}/commits", $commitQuery, []);
			$stats['errors'] = array_merge($stats['errors'], $commits['errors']);
			foreach ($commits['items'] as $commit) {
				if ($this->upsertCommit($localRepoId, $commit)) {
					$stats['commits_synced']++;
				}
			}

			$pullRequests = $this->fetchPaged("/repos/{$owner}/{$name}/pulls", ['state' => 'all', 'sort' => 'recentupdate', 'direction' => 'desc'], []);
			$stats['errors'] = array_merge($stats['errors'], $pullRequests['errors']);
			foreach ($pullRequests['items'] as $pullRequest) {
				if ($sinceIso !== null && !$this->isOnOrAfterSince($pullRequest['updated_at'] ?? null, $sinceIso)) {
					continue;
				}

				if ($this->upsertPullRequest($localRepoId, $pullRequest)) {
					$stats['pull_requests_synced']++;
				}
			}

			$this->repositoriesModel->update($localRepoId, [
				'last_synced_at' => date('Y-m-d H:i:s'),
			]);
		}

		$message = $sinceIso === null
			? 'Gitea full sync completed'
			: 'Gitea incremental sync completed (last ' . ($days ?? 1) . ' day)';

		return [
			'success' => true,
			'message' => $message,
			'stats' => $stats,
		];
	}

	private function fetchPaged(string $endpoint, array $query = [], array $listKeys = ['data']): array
	{
		$page = 1;
		$items = [];
		$errors = [];

		do {
			$payload = array_merge($query, [
				'limit' => $this->perPage,
				'page' => $page,
			]);

			$response = $this->request('GET', $endpoint, $payload);
			if ($response['success'] === false) {
				$errors[] = $response['message'];
				break;
			}

			$chunk = $this->extractList($response['data'], $listKeys);
			if (empty($chunk)) {
				break;
			}

			$items = array_merge($items, $chunk);
			$page++;
		} while (count($chunk) >= $this->perPage);

		return [
			'items' => $items,
			'errors' => $errors,
		];
	}

	private function extractList($data, array $listKeys): array
	{
		if (is_array($data) && array_is_list($data)) {
			return $data;
		}

		if (!is_array($data)) {
			return [];
		}

		foreach ($listKeys as $key) {
			if (isset($data[$key]) && is_array($data[$key])) {
				return $data[$key];
			}
		}

		if (isset($data['items']) && is_array($data['items'])) {
			return $data['items'];
		}

		return [];
	}

	private function upsertUser(array $user): ?int
	{
		$giteaUserId = isset($user['id']) ? (int) $user['id'] : null;
		$username = $user['username'] ?? $user['login'] ?? null;

		if ($giteaUserId === null || $username === null) {
			return null;
		}

		$data = [
			'gitea_user_id' => $giteaUserId,
			'username' => $username,
			'full_name' => $user['full_name'] ?? null,
			'email' => $user['email'] ?? null,
			'avatar_url' => $user['avatar_url'] ?? null,
			'is_active' => isset($user['active']) ? (int) $user['active'] : 1,
			'is_admin' => isset($user['is_admin']) ? (int) $user['is_admin'] : 0,
			'last_synced_at' => date('Y-m-d H:i:s'),
		];

		$existing = $this->usersModel->where('gitea_user_id', $giteaUserId)->first();
		if ($existing) {
			$this->usersModel->update((int) $existing['id'], $data);
			return (int) $existing['id'];
		}

		$this->usersModel->insert($data);
		return (int) $this->usersModel->getInsertID();
	}

	private function upsertRepository(array $repo): ?int
	{
		$giteaRepoId = isset($repo['id']) ? (int) $repo['id'] : null;
		if ($giteaRepoId === null) {
			return null;
		}

		$ownerUserId = null;
		if (isset($repo['owner']) && is_array($repo['owner'])) {
			$ownerUserId = $this->upsertUser($repo['owner']);
		}

		$data = [
			'gitea_repo_id' => $giteaRepoId,
			'owner_user_id' => $ownerUserId,
			'owner_username' => $repo['owner']['username'] ?? $repo['owner']['login'] ?? null,
			'name' => $repo['name'] ?? '',
			'full_name' => $repo['full_name'] ?? '',
			'description' => $repo['description'] ?? null,
			'html_url' => $repo['html_url'] ?? null,
			'clone_url' => $repo['clone_url'] ?? null,
			'default_branch' => $repo['default_branch'] ?? null,
			'is_private' => isset($repo['private']) ? (int) $repo['private'] : 0,
			'is_archived' => isset($repo['archived']) ? (int) $repo['archived'] : 0,
			'is_fork' => isset($repo['fork']) ? (int) $repo['fork'] : 0,
			'open_issues_count' => (int) ($repo['open_issues_count'] ?? 0),
			'stars_count' => (int) ($repo['stars_count'] ?? 0),
			'forks_count' => (int) ($repo['forks_count'] ?? 0),
			'watchers_count' => (int) ($repo['watchers_count'] ?? 0),
			'last_pushed_at' => $this->normalizeDate($repo['updated_at'] ?? $repo['pushed_at'] ?? null),
			'last_synced_at' => date('Y-m-d H:i:s'),
		];

		$existing = $this->repositoriesModel->where('gitea_repo_id', $giteaRepoId)->first();
		if ($existing) {
			$this->repositoriesModel->update((int) $existing['id'], $data);
			return (int) $existing['id'];
		}

		$this->repositoriesModel->insert($data);
		return (int) $this->repositoriesModel->getInsertID();
	}

	private function upsertCommit(int $repositoryId, array $commit): bool
	{
		$sha = $commit['sha'] ?? null;
		if ($sha === null) {
			return false;
		}

		$authorUserId = null;
		if (isset($commit['author']) && is_array($commit['author']) && isset($commit['author']['id'])) {
			$authorUserId = $this->upsertUser($commit['author']);
		}

		$data = [
			'repository_id' => $repositoryId,
			'author_user_id' => $authorUserId,
			'sha' => $sha,
			'short_sha' => substr($sha, 0, 10),
			'message' => $commit['commit']['message'] ?? null,
			'author_name' => $commit['commit']['author']['name'] ?? null,
			'author_email' => $commit['commit']['author']['email'] ?? null,
			'committed_at' => $this->normalizeDate($commit['commit']['author']['date'] ?? null),
			'html_url' => $commit['html_url'] ?? null,
		];

		$existing = $this->commitsModel
			->where('repository_id', $repositoryId)
			->where('sha', $sha)
			->first();
		if ($existing) {
			$this->commitsModel->update((int) $existing['id'], $data);
			return true;
		}

		$this->commitsModel->insert($data);
		return true;
	}

	private function upsertPullRequest(int $repositoryId, array $pullRequest): bool
	{
		$giteaPrId = isset($pullRequest['id']) ? (int) $pullRequest['id'] : null;
		if ($giteaPrId === null) {
			return false;
		}

		$authorUserId = null;
		if (isset($pullRequest['user']) && is_array($pullRequest['user']) && isset($pullRequest['user']['id'])) {
			$authorUserId = $this->upsertUser($pullRequest['user']);
		}

		$data = [
			'gitea_pr_id' => $giteaPrId,
			'repository_id' => $repositoryId,
			'author_user_id' => $authorUserId,
			'number' => (int) ($pullRequest['number'] ?? 0),
			'title' => $pullRequest['title'] ?? '',
			'body' => $pullRequest['body'] ?? null,
			'state' => $pullRequest['state'] ?? 'open',
			'is_draft' => isset($pullRequest['draft']) ? (int) $pullRequest['draft'] : 0,
			'is_merged' => isset($pullRequest['merged']) ? (int) $pullRequest['merged'] : 0,
			'merged_at' => $this->normalizeDate($pullRequest['merged_at'] ?? null),
			'closed_at' => $this->normalizeDate($pullRequest['closed_at'] ?? null),
			'created_at_gitea' => $this->normalizeDate($pullRequest['created_at'] ?? null),
			'updated_at_gitea' => $this->normalizeDate($pullRequest['updated_at'] ?? null),
			'html_url' => $pullRequest['html_url'] ?? null,
		];

		$existing = $this->pullRequestsModel->where('gitea_pr_id', $giteaPrId)->first();
		if ($existing) {
			$this->pullRequestsModel->update((int) $existing['id'], $data);
			return true;
		}

		$this->pullRequestsModel->insert($data);
		return true;
	}

	private function isOnOrAfterSince(?string $value, string $sinceIso): bool
	{
		if ($value === null || $value === '') {
			return false;
		}

		try {
			return (new \DateTimeImmutable($value)) >= (new \DateTimeImmutable($sinceIso));
		} catch (\Throwable $e) {
			return false;
		}
	}

	private function request(string $method, string $endpoint, array $query = []): array
	{
		$url = $this->baseUrl . $endpoint;
		if (!empty($query)) {
			$url .= '?' . http_build_query($query);
		}

		$client = \Config\Services::curlrequest();

		try {
			$response = $client->request($method, $url, [
				'headers' => [
					'Authorization' => 'token ' . $this->token,
					'Accept' => 'application/json',
				],
				'http_errors' => false,
			]);

			$statusCode = $response->getStatusCode();
			$body = json_decode($response->getBody(), true);

			if ($statusCode < 200 || $statusCode >= 300) {
				return [
					'success' => false,
					'message' => 'Gitea request failed [' . $statusCode . '] ' . $endpoint,
					'data' => $body,
				];
			}

			return [
				'success' => true,
				'message' => 'ok',
				'data' => $body,
			];
		} catch (\Throwable $e) {
			return [
				'success' => false,
				'message' => 'Gitea request exception: ' . $e->getMessage(),
				'data' => null,
			];
		}
	}

	private function normalizeDate(?string $value): ?string
	{
		if ($value === null || $value === '') {
			return null;
		}

		try {
			return (new \DateTime($value))->format('Y-m-d H:i:s');
		} catch (\Throwable $e) {
			return null;
		}
	}
}
