<?php
/**
 * Git provider abstraction for code-block source linking. Each provider knows how
 * to recognise its own URLs, parse them into parts, and build raw-fetch + human
 * view URLs (with line anchors). The registry picks the first provider that matches.
 *
 * Pure string logic only, no network, no DB, no WordPress core calls inside the
 * providers themselves (the registry uses apply_filters). That keeps every
 * provider unit-testable in plain PHP; see tests/git-provider-test.php.
 *
 * @package CloudScale_DevTools
 * @since   1.10.150
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; }

interface CSDT_Git_Provider {

	/** Stable slug stored/compared internally, e.g. 'github', 'gitlab'. */
	public function id(): string;

	/** Human label for UI ("GitHub", "GitLab"). */
	public function label(): string;

	/** True when this provider recognises the given URL. */
	public function matches( string $url ): bool;

	/**
	 * Parse a blob/source URL into parts, or null when it doesn't match.
	 *
	 * @return array{host:string,owner:string,repo:string,branch:string,path:string,snippet:string}|null
	 */
	public function parse( string $url ): ?array;

	/** Build the unauthenticated raw-content URL for a parsed parts array. */
	public function raw_url( array $parts ): string;

	/**
	 * Build the human "view source" URL. Appends a line anchor when $line_start > 0;
	 * pass 0 to omit the anchor (whole-file links).
	 */
	public function view_url( array $parts, int $line_start, int $line_end ): string;
}

/**
 * Registry + shared helpers. Detection is first-match-wins over the provider list,
 * which is filterable via `csdt_git_providers` so future providers (Bitbucket,
 * self-hosted Gitea hosts) can be added without editing this file.
 */
final class CSDT_Git_Providers {

	/** @var CSDT_Git_Provider[]|null */
	private static ?array $registry = null;

	/**
	 * Lazily builds and caches the provider list.
	 *
	 * @return CSDT_Git_Provider[]
	 */
	private static function registry(): array {
		if ( self::$registry === null ) {
			$providers = array(
				new CSDT_Provider_GitHub(),
				new CSDT_Provider_GitLab(),
			);
			/**
			 * Filter the registered git providers. Order matters: detection returns
			 * the first provider whose matches() is true.
			 *
			 * @param CSDT_Git_Provider[] $providers
			 */
			self::$registry = apply_filters( 'csdt_git_providers', $providers );
		}
		return self::$registry;
	}

	/** Returns the first provider that recognises the URL, or null. */
	public static function detect( string $url ): ?CSDT_Git_Provider {
		foreach ( self::registry() as $p ) {
			if ( $p->matches( $url ) ) {
				return $p;
			}
		}
		return null;
	}

	/** Look up a provider by its slug. */
	public static function by_id( string $id ): ?CSDT_Git_Provider {
		foreach ( self::registry() as $p ) {
			if ( $p->id() === $id ) {
				return $p;
			}
		}
		return null;
	}

	/** rawurlencode each path segment but keep the slashes between them. */
	public static function encode_path( string $path ): string {
		return implode( '/', array_map( 'rawurlencode', explode( '/', $path ) ) );
	}

	/** Pull a #snippet=NAME hint out of a URL hash fragment. */
	public static function snippet_from_hash( string $hash ): string {
		if ( preg_match( '/(?:^|[&;])snippet=([A-Za-z0-9_.\-]+)/', $hash, $sm ) ) {
			return $sm[1];
		}
		return '';
	}
}

/**
 * GitHub provider. URL shape:
 *   view: https://github.com/{owner}/{repo}/blob/{branch}/{path}#L42-L87
 *   raw:  https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}
 */
final class CSDT_Provider_GitHub implements CSDT_Git_Provider {

	public function id(): string {
		return 'github'; }

	public function label(): string {
		return 'GitHub'; }

	public function matches( string $url ): bool {
		return (bool) preg_match( '~^https?://github\.com/~', trim( $url ) );
	}

	public function parse( string $url ): ?array {
		$url = trim( $url );
		if ( ! preg_match( '~^https?://github\.com/([^/\s]+)/([^/\s]+)/blob/([^/\s]+)/([^#\s]+)(?:#(.*))?$~', $url, $m ) ) {
			return null;
		}
		return array(
			'host'    => 'github.com',
			'owner'   => $m[1],
			'repo'    => $m[2],
			'branch'  => rawurldecode( $m[3] ),
			'path'    => rawurldecode( $m[4] ),
			'snippet' => CSDT_Git_Providers::snippet_from_hash( $m[5] ?? '' ),
		);
	}

	public function raw_url( array $p ): string {
		return sprintf(
			'https://raw.githubusercontent.com/%s/%s/%s/%s',
			rawurlencode( $p['owner'] ),
			rawurlencode( $p['repo'] ),
			rawurlencode( $p['branch'] ),
			CSDT_Git_Providers::encode_path( $p['path'] )
		);
	}

	public function view_url( array $p, int $line_start, int $line_end ): string {
		$url = sprintf(
			'https://github.com/%s/%s/blob/%s/%s',
			rawurlencode( $p['owner'] ),
			rawurlencode( $p['repo'] ),
			rawurlencode( $p['branch'] ),
			CSDT_Git_Providers::encode_path( $p['path'] )
		);
		if ( $line_start > 0 ) {
			$url .= '#L' . $line_start . '-L' . $line_end;
		}
		return $url;
	}
}

/**
 * GitLab provider, works on gitlab.com AND self-hosted instances. GitLab's
 * `/-/blob/` path segment is distinctive, so we recognise it on any host rather
 * than matching a fixed hostname. Owner may contain nested subgroups
 * (group/subgroup/repo), which we preserve.
 *
 *   view: https://{host}/{owner}/{repo}/-/blob/{branch}/{path}#L42-87
 *   raw:  https://{host}/{owner}/{repo}/-/raw/{branch}/{path}
 *
 * Note: only PUBLIC repos are supported (unauthenticated raw fetch). Private /
 * token-gated instances are intentionally out of scope.
 */
final class CSDT_Provider_GitLab implements CSDT_Git_Provider {

	public function id(): string {
		return 'gitlab'; }

	public function label(): string {
		return 'GitLab'; }

	public function matches( string $url ): bool {
		return (bool) preg_match( '~^https?://[^/\s]+/.+/-/blob/~', trim( $url ) );
	}

	public function parse( string $url ): ?array {
		$url = trim( $url );
		// owner/repo is everything before `/-/blob/`; capture lazily then split off
		// the final segment as the repo so nested subgroups stay in the owner.
		if ( ! preg_match( '~^https?://([^/\s]+)/(.+?)/-/blob/([^/\s]+)/([^#\s]+)(?:#(.*))?$~', $url, $m ) ) {
			return null;
		}
		$ns    = explode( '/', $m[2] );
		$repo  = array_pop( $ns );
		$owner = implode( '/', $ns );
		if ( $owner === '' || $repo === '' ) {
			return null;
		}
		return array(
			'host'    => $m[1],
			'owner'   => $owner,
			'repo'    => $repo,
			'branch'  => rawurldecode( $m[3] ),
			'path'    => rawurldecode( $m[4] ),
			'snippet' => CSDT_Git_Providers::snippet_from_hash( $m[5] ?? '' ),
		);
	}

	public function raw_url( array $p ): string {
		return sprintf(
			// Host is emitted verbatim, it may carry a :port and is already
			// constrained to a single hostname token by parse()'s regex.
			'https://%s/%s/%s/-/raw/%s/%s',
			$p['host'],
			CSDT_Git_Providers::encode_path( $p['owner'] ),
			rawurlencode( $p['repo'] ),
			rawurlencode( $p['branch'] ),
			CSDT_Git_Providers::encode_path( $p['path'] )
		);
	}

	public function view_url( array $p, int $line_start, int $line_end ): string {
		$url = sprintf(
			'https://%s/%s/%s/-/blob/%s/%s',
			$p['host'],
			CSDT_Git_Providers::encode_path( $p['owner'] ),
			rawurlencode( $p['repo'] ),
			rawurlencode( $p['branch'] ),
			CSDT_Git_Providers::encode_path( $p['path'] )
		);
		if ( $line_start > 0 ) {
			// GitLab uses a single-dash line range, e.g. #L42-87.
			$url .= '#L' . $line_start . '-' . $line_end;
		}
		return $url;
	}
}
