import * as Variables from "./Variables.js";

export async function GetTimeline(Cursor) {
	if (localStorage.getItem(Variables.BlueskyAccessToken) == null) {
		console.log("No access token!");
		return "";
	}
	
	let PDS = localStorage.getItem(Variables.BlueskyPDS);
	let DPoP;
	let request;
	if (Cursor == "") {
		DPoP = await ClientDPoPPDS("GET", PDS + "/xrpc/app.bsky.feed.getTimeline");
		request = fetch(PDS + "/xrpc/app.bsky.feed.getTimeline", {method: "GET", headers: {"Authorization": "DPoP " + localStorage.getItem(Variables.BlueskyAccessToken), "DPoP": DPoP}});
	} else {
		DPoP = await ClientDPoPPDS("GET", PDS + "/xrpc/app.bsky.feed.getTimeline?cursor=" + Cursor);
		request = fetch(PDS + "/xrpc/app.bsky.feed.getTimeline?cursor=" + Cursor, {method: "GET", headers: {"Authorization": "DPoP " + localStorage.getItem(Variables.BlueskyAccessToken), "DPoP": DPoP}});
	}
	let body = await request.then((response) => response.json());
	let status = await request.then((response) => response.status);
	let header = await request.then((response) => response.headers.get("dpop-nonce"));
	if (status == 401) {
		await HandleError(body, header);
		body = await GetTimeline(Cursor);
	}
	return body;
}

export async function GetPosts(URIs) {
	if (localStorage.getItem(Variables.BlueskyAccessToken) == null) {
		console.log("No access token!");
		return "";
	}
	
	let PDS = localStorage.getItem(Variables.BlueskyPDS);
	let DPoP = await ClientDPoPPDS("GET", PDS + "/xrpc/app.bsky.feed.getPosts?uris=" + URIs);
	let request = fetch(PDS + "/xrpc/app.bsky.feed.getPosts?uris=" + URIs, { method: "GET", headers: {"Authorization": "DPoP " + localStorage.getItem(Variables.BlueskyAccessToken), "DPoP": DPoP}});
	let body = await request.then((response) => response.json());
	let status = await request.then((response) => response.status);
	let header = await request.then((response) => response.headers.get("dpop-nonce"));
	if (status == 401) {
		await HandleError(body, header);
		body = await GetPosts(URIs);
	}
	return body;
}

// Get a blob (like an image or video). Authentication need not apply.
export async function GetBlob(DID, CID) {
	let request = fetch("https://bsky.social/xrpc/com.atproto.sync.getBlob?did=" + DID + "&cid=" + CID, {method: "GET"});
	let body = await request.then((response) => response.json());
	return body;
}

export async function CreatePost(DID, Text) {
	if (localStorage.getItem(Variables.BlueskyAccessToken) == null) {
		console.log("No access token!");
		return "";
	}
	let Record = {
		"$type": "app.bsky.feed.post",
		"text": Text,
		"createdAt": new Date(Date.now()).toISOString()
	}
	let body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined);
	if (body.hasOwnProperty("error") && body.error == "InvalidRequest") {
		let matches = body.message.match(/(\d+)/);
		Record.text = Text.slice(0, matches[0] - 1);
		body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined);
		await CreateReplyPost(DID, Text.slice(matches[0] - 1, Text.length - 1), body, body);
	}
	return body;
}

export async function CreateReplyPost(DID, Text, ReplyID, RootID) {
	if (localStorage.getItem(Variables.BlueskyAccessToken) == null) {
		console.log("No access token!");
		return "";
	}
	let Record = {
		"$type": "app.bsky.feed.post",
		"text": Text,
		"createdAt": new Date(Date.now()).toISOString(),
		"reply": {
			"parent": {
				"uri": ReplyID.uri,
				"cid": ReplyID.cid
			},
			"root": {
				"uri": RootID.uri,
				"cid": RootID.cid
			}
		}
	}
	let body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined);
	if (body.hasOwnProperty("error") && body.error == "InvalidRequest") {
		let matches = body.message.match(/(\d+)/);
		Record.text = Text.slice(0, matches[0] - 1);
		body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined);
		await CreateReplyPost(DID, Text.slice(matches[0] - 1, Text.length - 1), body, RootID);
	}
	return body;
}

export async function SetThreadGate(DID, Post, VisibilitySettings) {
	if (localStorage.getItem(Variables.BlueskyAccessToken) == null) {
		console.log("No access token!");
		return "";
	}
	let Record = {
		"$type": "app.bsky.feed.threadgate",
		"post": Post,
		"allow": VisibilitySettings,
		"createdAt": new Date(Date.now()).toISOString()
	}
	let body = CreateRecord(DID, "app.bsky.feed.threadgate", Record, undefined);
	return body;
}

export async function SendLike(DID, RefURI, RefCID) {
	if (localStorage.getItem(Variables.BlueskyAccessToken) == null) {
		console.log("No access token!");
		return "";
	}
	let StrongRef = {
		"$type": "com.atproto.repo.strongRef",
		"uri": RefURI,
		"cid": RefCID
	}
	let Record = {
		"$type": "app.bsky.feed.like",
		"subject": StrongRef,
		"createdAt": new Date(Date.now()).toISOString()
	}
	let body = await GetRecord(DID, "app.bsky.feed.like", RefURI.split("/")[RefURI.split("/").length - 1]);
	// We might have a record. If we do, delete the thing. If we don't, create the thing.
	if (body.hasOwnProperty("error") && body.error == "RecordNotFound") {
		body = await CreateRecord(DID, "app.bsky.feed.like", Record, RefURI.split("/")[RefURI.split("/").length - 1]);
	} else {
		body = await DeleteRecord(DID, "app.bsky.feed.like", RefURI.split("/")[RefURI.split("/").length - 1]);
	}
	return body;
}

export async function SendRepost(DID, RefURI, RefCID) {
	if (localStorage.getItem(Variables.BlueskyAccessToken) == null) {
		console.log("No access token!");
		return "";
	}
	let StrongRef = {
		"$type": "com.atproto.repo.strongRef",
		"uri": RefURI,
		"cid": RefCID
	}
	let Record = {
		"$type": "app.bsky.feed.repost",
		"subject": StrongRef,
		"createdAt": new Date(Date.now()).toISOString()
	}
	let body = await GetRecord(DID, "app.bsky.feed.repost", RefURI.split("/")[RefURI.split("/").length - 1]);
	// We might have a record. If we do, delete the thing. If we don't, create the thing.
	if (body.hasOwnProperty("error") && body.error == "RecordNotFound") {
		body = await CreateRecord(DID, "app.bsky.feed.repost", Record, RefURI.split("/")[RefURI.split("/").length - 1]);
	} else {
		body = await DeleteRecord(DID, "app.bsky.feed.repost", RefURI.split("/")[RefURI.split("/").length - 1]);
	}
	return body;
}

export async function GetRecord(Repo, Collection, RKey) {
	let RequestBody = {
		"repo": Repo,
		"collection": Collection,
		"rkey": RKey
	}
	let DPoP = await ClientDPoPPDS("GET", localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.getRecord?repo=" + Repo + "&collection=" + Collection + "&rkey=" + RKey);
	let request = fetch(localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.getRecord?repo=" + Repo + "&collection=" + Collection + "&rkey=" + RKey, { method: "GET", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + localStorage.getItem(Variables.BlueskyAccessToken), "DPoP": DPoP}});
	let body = await request.then((response) => response.json());
	let status = await request.then((response) => response.status);
	let header = await request.then((response) => response.headers.get("dpop-nonce"));
	if (status == 401) {
		await HandleError(body, header);
		body = await DeleteRecord(Repo, Collection, RKey);
	}
	return body;
}

export async function CreateRecord(Repo, Collection, Record, RKey) {
	let RequestBody = {
		"repo": Repo,
		"collection": Collection,
		"record": Record
	}
	if (RKey != undefined) {
		RequestBody.rkey = RKey;
	}
	let DPoP = await ClientDPoPPDS("POST", localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.createRecord");
	let request = fetch(localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.createRecord", { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + localStorage.getItem(Variables.BlueskyAccessToken), "DPoP": DPoP}});
	let body = await request.then((response) => response.json());
	let status = await request.then((response) => response.status);
	let header = await request.then((response) => response.headers.get("dpop-nonce"));
	if (status == 401) {
		await HandleError(body, header);
		body = await CreateRecord(Repo, Collection, Record, RKey);
	}
	return body;
}

export async function DeleteRecord(Repo, Collection, RKey) {
	let RequestBody = {
		"repo": Repo,
		"collection": Collection,
		"rkey": RKey
	}
	let DPoP = await ClientDPoPPDS("POST", localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.deleteRecord");
	let request = fetch(localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.deleteRecord", { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + localStorage.getItem(Variables.BlueskyAccessToken), "DPoP": DPoP}});
	let body = await request.then((response) => response.json());
	let status = await request.then((response) => response.status);
	let header = await request.then((response) => response.headers.get("dpop-nonce"));
	if (status == 401) {
		await HandleError(body, header);
		body = await DeleteRecord(Repo, Collection, RKey);
	}
	return body;
}

// Component 1/4
export async function GetPDSWellKnown() {
	return await fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"})
	.then((response) => response.json());
}

// Component 2/4
// Many thanks to https://github.com/tonyxu-io/pkce-generator. It was the base for this code.
export async function CreatePKCECodeVerifier() {
	// Generate some Numbers
	let Numbers = new Uint8Array(32);
	crypto.getRandomValues(Numbers);
	// Generate a random string of characters.
	let CodeVerifier = "";
	for (let i in Numbers) {
		CodeVerifier += String.fromCharCode(Numbers[i]);
	}
	// Put this random string into Base64URL format.
	CodeVerifier = btoa(CodeVerifier).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
	return CodeVerifier;
}

export async function CreatePKCECodeChallenge(CodeVerifier) {
	// Generate a code challenge with the code verifier.
	// This is done by first SHA256 encrypting the CodeVerifier, then putting the outputted string into Base64URL format.
	let CodeChallenge = btoa(await sha256(CodeVerifier)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
	return CodeChallenge;
}

// Component 3/4
export async function PARrequest(PAREndpoint, State, Challenge) {
	return fetch(PAREndpoint, {method: "POST", body: new URLSearchParams({ response_type: "code", code_challenge_method: "S256", scope: "atproto transition:generic", client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_challenge: Challenge, state: State, login_hint: "crowdedgames.group" }), headers: {"Content-Type": "application/x-www-form-urlencoded"}});
}

async function AuthRequest(TokenEndpoint, code, DPoP, Verify) {
	return fetch(TokenEndpoint, {method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", code: code, client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_verifier: Verify}), headers: { "DPoP": DPoP, "Content-Type": "application/x-www-form-urlencoded"}})
	.then((response) => response.json());
}

async function ReauthRequest(TokenEndpoint, Token, DPoP) {
	return fetch(TokenEndpoint, {method: "POST", body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: Token, client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json"}), headers: { "DPoP": DPoP, "Content-Type": "application/x-www-form-urlencoded"}});
}

// Component 4/4
export async function ClientDPoPToken(POSTorGET, RequestURL) {
	let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPublicKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]);
	let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPrivateKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]);
	
	// Header
	var Header = {typ: "dpop+jwt", alg: "ES256", jwk: 
		await crypto.subtle.exportKey("jwk", PublicKey)
		.then(function(response) {
			delete response["key_ops"];
			delete response["ext"];
			delete response["alg"];
			return response})
		};
	// Payload
	var Payload = {};
	Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json";
	Payload.jti = crypto.randomUUID();
	Payload.htm = POSTorGET;
	Payload.htu = RequestURL;
	Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000);
	Payload.nonce = localStorage.getItem(Variables.BlueskyNonce);

	var sHeader = JSON.stringify(Header);
	var sPayload = JSON.stringify(Payload);
	var JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload, 
	await crypto.subtle.exportKey("jwk", PrivateKey)
	.then(function(response) {
		delete response["key_ops"];
		delete response["ext"];
		delete response["alg"];
		return response})
	);
	return JWT;
}

export async function ClientDPoPPDS(POSTorGET, RequestURL) {
	let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPublicKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]);
	let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPrivateKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]);
	
	// Header
	var Header = {typ: "dpop+jwt", alg: "ES256", jwk: 
		await crypto.subtle.exportKey("jwk", PublicKey)
		.then(function(response) {
			delete response["key_ops"];
			delete response["ext"];
			delete response["alg"];
			return response})
		};
	// Payload
	var Payload = {};
	Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json";
	Payload.jti = crypto.randomUUID();
	Payload.htm = POSTorGET;
	Payload.htu = RequestURL;
	Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000);
	Payload.nonce = localStorage.getItem(Variables.BlueskyNonce);
	Payload.ath = await CreatePKCECodeChallenge(localStorage.getItem(Variables.BlueskyAccessToken));

	var sHeader = JSON.stringify(Header);
	var sPayload = JSON.stringify(Payload);
	var JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload, 
	await crypto.subtle.exportKey("jwk", PrivateKey)
	.then(function(response) {
		delete response["key_ops"];
		delete response["ext"];
		delete response["alg"];
		return response})
	);
	return JWT;
}

export async function HandleAuthorization(Website) {
	// Quickly check to see if it has something before :// so it doesn't screw the link.
	if (Website.toLowerCase().split("://").length > 1) {
		Website = "https://" + Website.split("://")[1];
	} else {
		Website = "https://" + Website;
	}
	// Declare Variables
	let KeyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]);
	
	let WellKnown = await GetPDSWellKnown();
	
	let State = crypto.randomUUID();
	
	let PKCEverifier = await CreatePKCECodeVerifier();
	let PKCEchallenge = await CreatePKCECodeChallenge(PKCEverifier);
	// Save these
	localStorage.setItem(Variables.BlueskyPKCEVerifier, PKCEverifier);
	localStorage.setItem(Variables.BlueskyPKCEChallenge, PKCEchallenge);
	// PAR request (beginning)
	let PAR = PARrequest(WellKnown.pushed_authorization_request_endpoint, State, PKCEchallenge);
	let body = await PAR.then((response) => response.json());
	let nonce = await PAR.then((response) => response.headers.get("dpop-nonce"));
	// Save nonce
	localStorage.setItem(Variables.BlueskyNonce, nonce);
	// Export keys
	let ExportedKey1 = await crypto.subtle.exportKey("jwk", KeyPair.publicKey);
	let ExportedKey2 = await crypto.subtle.exportKey("jwk", KeyPair.privateKey);
	// Convert them into a good format.
	localStorage.setItem(Variables.BlueskyPublicKey, JSON.stringify(ExportedKey1));
	localStorage.setItem(Variables.BlueskyPrivateKey, JSON.stringify(ExportedKey2));
	// Now we need to authenticate. Make sure the State stays the same throughout this whole process :]
	document.location.href = Website + "/oauth/authorize?client_id=https://fedi.crowdedgames.group/oauth/client-metadata.json&request_uri=" + body.request_uri;
}

export async function GainTokens() {
	let WellKnown = await GetPDSWellKnown();
	
	// Check to see if something's a miss...
	if ((document.location.href.split("state=").length > 1 && document.location.href.split("iss=").length > 1 && document.location.href.split("code=").length > 1) && localStorage.getItem(Variables.BlueskyPKCEVerifier) != null && localStorage.getItem(Variables.BlueskyAccessToken) == null) {
		// Create varaibles, be aware of waits because of internet.
		let DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint);
		let code = document.location.href.split("code=")[1];
		// Authentication
		let Var = await localStorage.getItem(Variables.BlueskyPKCEVerifier);
		let Auth = await AuthRequest(WellKnown.token_endpoint, code, DPoP, Var);
		// Save the tokens and be done
		localStorage.setItem(Variables.BlueskyAccessToken, Auth.access_token);
		localStorage.setItem(Variables.BlueskyRefreshToken, Auth.refresh_token);
		// That long string just gets the payload
		// aud = PDS server we are communicating with; sub = user DID
		localStorage.setItem(Variables.BlueskyPDS, "https://" + KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).aud.split(":")[2]);
		localStorage.setItem(Variables.BlueskyDID, KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).sub);
	}
}

// Refreshing tokens is an integral part of auth.
export async function RefreshTokens() {
	let WellKnown = await GetPDSWellKnown();
	// Create varaibles, be aware of waits because of internet.
	let DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint);
	// Token refresh
	let Var = await localStorage.getItem(Variables.BlueskyRefreshToken);
	let Auth = ReauthRequest(WellKnown.token_endpoint, Var, DPoP);
	let body = await Auth.then((response) => response.json());
	let header = await Auth.then((response) => response.headers.get("dpop-nonce"));
	if (body.hasOwnProperty("error_description") && body.error_description.includes("DPoP nonce mismatch")) {
		localStorage.setItem(Variables.BlueskyNonce, header);
		DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint);
		body = await ReauthRequest(WellKnown.token_endpoint, Var, DPoP).then((response) => response.json());
	}
	// Save the tokens and be done
	localStorage.setItem(Variables.BlueskyAccessToken, body.access_token);
	localStorage.setItem(Variables.BlueskyRefreshToken, body.refresh_token);
}

async function HandleError(body, header) {
	if (body.message.includes("DPoP nonce mismatch")) {
		await localStorage.setItem(Variables.BlueskyNonce, header);
	}
	if (body.message.includes("claim timestamp check failed")) {
		await RefreshTokens();
	}
}

// Stolen from elsewhere.
// Firefox snippet; Slightly edited.
async function sha256(message) {
	// encode as UTF-8
	const MessageBuffer = new TextEncoder().encode(message);                    
	
	// hash the message
	const HashBuffer = await crypto.subtle.digest('SHA-256', MessageBuffer);
	
	// convert ArrayBuffer to Array
	const HashArray = Array.from(new Uint8Array(HashBuffer));
	
	// convert this hashArray to a string
	let string = "";
	for (let i in HashArray) {
		string += String.fromCharCode(HashArray[i]);
	}
	
	return string;
}

// Firefox snippet.
function ab2str(buf) {
  return String.fromCharCode.apply(null, new Uint8Array(buf));
}

// Firefox snippet.
function str2ab(str) {
  const buf = new ArrayBuffer(str.length);
  const bufView = new Uint8Array(buf);
  for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}