/**
 * DID Creation and Registration Flow
 * @author Matthew Raymer
 * 
 * This module implements the creation and registration of Decentralized Identifiers (DIDs)
 * with the endorser.ch service. It matches the Python implementation in new_flow.py.
 */

import { HDNode } from '@ethersproject/hdnode';
import { Wallet } from '@ethersproject/wallet';
import { createJWT, ES256KSigner, SimpleSigner } from 'did-jwt';
import axios from 'axios';
import { mkdir, writeFile } from 'fs/promises';
import { join } from 'path';

// Constants
const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
const API_SERVER = "https://test-api.endorser.ch";
const ENDORSER_DID = "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F";
const ENDORSER_PRIVATE_KEY = "2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b";

interface IKey {
    id: string;
    type: string;
    controller: string;
    ethereumAddress: string;
    publicKeyHex: string;
    privateKeyHex: string;
}

interface IIdentifier {
    did: string;
    keys: IKey[];
    services: any[];
}

// Create .generated directory if it doesn't exist
const GENERATED_DIR = '.generated';

// Initialize directory creation
async function initializeDirectories() {
    try {
        await mkdir(GENERATED_DIR, { recursive: true });
    } catch (err) {
        console.error('Error creating .generated directory:', err);
        throw err;
    }
}

/**
 * Generate a new mnemonic seed phrase
 */
function generateMnemonic(): string {
    return Wallet.createRandom().mnemonic.phrase;
}

/**
 * Derive Ethereum address and keys from a mnemonic phrase
 */
async function deriveAddress(
    mnemonic: string,
    derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH
): Promise<[string, string, string, string]> {
    mnemonic = mnemonic.trim().toLowerCase();
    const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
    const rootNode: HDNode = hdnode.derivePath(derivationPath);
    
    console.log("\nKey Derivation:");
    console.log(`  Mnemonic (first 4 words): ${mnemonic.split(' ').slice(0,4).join(' ')}...`);
    console.log(`  Derivation Path: ${derivationPath}`);
    
    const privateHex = rootNode.privateKey.substring(2); // remove '0x'
    const publicHex = rootNode.publicKey.substring(2); // remove '0x'
    const address = rootNode.address;

    console.log(`  Address: ${address}`);
    console.log(`  Private Key: ${privateHex.substring(0,8)}...`);
    console.log(`  Public Key: ${publicHex.substring(0,8)}...`);

    // Save derivation data
    const derivationData = {
        mnemonic,
        derivationPath,
        address,
        privateKey: privateHex,
        publicKey: publicHex
    };
    try {
        await mkdir(GENERATED_DIR, { recursive: true });
        await writeFile(
            join(GENERATED_DIR, 'key_derivation.json'),
            JSON.stringify(derivationData, null, 2)
        );
    } catch (err) {
        console.error('Error saving derivation data:', err);
    }

    return [address, privateHex, publicHex, derivationPath];
}

/**
 * Create a new DID identifier with associated keys
 */
function newIdentifier(
    address: string,
    publicKeyHex: string,
    privateKeyHex: string,
    derivationPath: string
): IIdentifier {
    const did = `did:ethr:${address}`;
    const keyId = `${did}#keys-1`;
    
    const identifier: IIdentifier = {
        did,
        keys: [{
            id: keyId,
            type: "Secp256k1VerificationKey2018",
            controller: did,
            ethereumAddress: address,
            publicKeyHex,
            privateKeyHex
        }],
        services: []
    };
    return identifier;
}

/**
 * Initialize a new DID account or load existing one
 */
async function initializeAccount(): Promise<IIdentifier> {
    const mnemonic = generateMnemonic();
    const [address, privateHex, publicHex, derivationPath] = await deriveAddress(mnemonic);
    const identity = newIdentifier(address, publicHex, privateHex, derivationPath);

    // Format and display account data
    const accountData = {
        did: identity.did,
        identity,
        mnemonic,
        derivation_path: derivationPath
    };
    console.log("\nAccount initialized:");
    console.log(JSON.stringify(accountData, null, 2));
    console.log();

    // Save account data
    await writeFile(
        join(GENERATED_DIR, 'account_init.json'),
        JSON.stringify(accountData, null, 2)
    );

    return identity;
}

/**
 * Create a signed JWT for DID registration
 */
async function createEndorserJwt(
    did: string,
    privateKeyHex: string,
    payload: any,
    subDid?: string,
    expiresIn: number = 3600
): Promise<string> {
    const signer = await SimpleSigner(privateKeyHex);
    
    const jwt = await createJWT(
        payload,
        {
            issuer: did,
            signer,
            expiresIn,
            ...(subDid && { subject: subDid })
        }
    );
    
    return jwt;
}

/**
 * Register a DID with the endorser service
 */
async function register(
    activeDid: string,
    privateKeyHex: string,
    apiServer: string = API_SERVER
): Promise<{
    success?: boolean;
    handleId?: string;
    registrationId?: number;
    claimId?: string;
    error?: string;
    details?: any;
}> {
    console.log("\nEndorser DID:", ENDORSER_DID);
    console.log("Active DID:", activeDid);

    const vcPayload = {
        vc: {
            "@context": ["https://www.w3.org/2018/credentials/v1"],
            type: ["VerifiableCredential"],
            credentialSubject: {
                "@context": "https://schema.org",
                "@type": "RegisterAction",
                "agent": {
                    "identifier": ENDORSER_DID
                },
                "participant": {
                    "identifier": activeDid
                },
                "object": "endorser.ch",
                "endTime": Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
            }
        }
    };

    // Debug output
    console.log("\nRegistration Claim:");
    console.log(JSON.stringify(vcPayload, null, 2));
    console.log();

    // Sign with endorser's DID and private key
    const jwtToken = await createEndorserJwt(
        ENDORSER_DID,
        ENDORSER_PRIVATE_KEY,
        vcPayload,
        activeDid
    );

    // Debug output
    console.log("Generated JWT:");
    console.log(jwtToken);
    console.log();

    // Debug JWT parts
    const [header, payload, signature] = jwtToken.split('.');
    console.log("\nJWT Header:", JSON.parse(Buffer.from(header, 'base64url').toString()));
    console.log("JWT Payload:", JSON.parse(Buffer.from(payload, 'base64url').toString()));
    console.log("JWT Signature length:", Buffer.from(signature, 'base64url').length, "bytes");
    console.log();

    try {
        const response = await axios.post(
            `${apiServer}/api/v2/claim`,
            { jwtEncoded: jwtToken },
            { headers: { 'Content-Type': 'application/json' } }
        );

        if (response.status === 200 && response.data.success?.handleId) {
            // Return all success details
            const registrationData = {
                activeDid,
                jwtToken,
                response: response.data.success
            };
            await writeFile(
                join(GENERATED_DIR, 'registration.json'),
                JSON.stringify(registrationData, null, 2)
            );
            return {
                success: true,
                handleId: response.data.success.handleId,
                registrationId: response.data.success.registrationId,
                claimId: response.data.success.claimId
            };
        }

        // Log full error response
        console.error("\nAPI Error Response:");
        console.error(JSON.stringify(response.data, null, 2));
        console.error();

        // If we have success data but no handleId, it might be a partial success
        if (response.data.success) {
            const registrationData = {
                activeDid,
                jwtToken,
                response: response.data.success
            };
            await writeFile(
                join(GENERATED_DIR, 'registration.json'),
                JSON.stringify(registrationData, null, 2)
            );
            return {
                success: true,
                ...response.data.success
            };
        } else {
            const registrationData = {
                activeDid,
                jwtToken,
                response: response.data
            };
            await writeFile(
                join(GENERATED_DIR, 'registration.json'),
                JSON.stringify(registrationData, null, 2)
            );
            return { error: "Registration failed", details: response.data };
        }
    } catch (error: any) {
        // Log detailed error information
        console.error("\nAPI Request Error:");
        if (error.response) {
            console.error("Status:", error.response.status);
            console.error("Headers:", error.response.headers);
            console.error("Data:", JSON.stringify(error.response.data, null, 2));
        } else if (error.request) {
            console.error("No response received");
            console.error(error.request);
        } else {
            console.error("Error setting up request:", error.message);
        }
        console.error();

        const registrationData = {
            activeDid,
            jwtToken,
            response: { error: `Error submitting claim: ${error.message}` }
        };
        await writeFile(
            join(GENERATED_DIR, 'registration.json'),
            JSON.stringify(registrationData, null, 2)
        );
        return { error: `Error submitting claim: ${error.message}` };
    }
}

/**
 * Fetch claim details from the endorser service
 */
async function fetchClaim(
    claimId: string,
    activeDid: string,
    privateKeyHex: string,
    apiServer: string = API_SERVER
): Promise<any> {
    // Create authentication JWT
    const authPayload = {
        intent: 'claim.read',
        claimId: claimId
    };

    // Sign with the active DID (not endorser DID)
    const authToken = await createEndorserJwt(
        activeDid,
        privateKeyHex,
        authPayload
    );

    try {
        const response = await axios.get(
            `${apiServer}/api/claim/byHandle/${claimId}`,
            {
                headers: {
                    'Authorization': `Bearer ${authToken}`,
                    'Content-Type': 'application/json'
                }
            }
        );

        const claimData = response.data;
        
        // Save claim data
        try {
            await writeFile(
                join(GENERATED_DIR, 'claim_details.json'),
                JSON.stringify({
                    claim_id: claimId,
                    active_did: activeDid,
                    response: claimData
                }, null, 2)
            );
        } catch (err) {
            console.error('Error saving claim data:', err);
        }
        
        return claimData;

    } catch (error: any) {
        console.error("\nError fetching claim:");
        if (error.response) {
            console.error("Status:", error.response.status);
            console.error("Data:", JSON.stringify(error.response.data, null, 2));
        } else {
            console.error(error.message);
        }
        // Save error state
        try {
            await writeFile(
                join(GENERATED_DIR, 'claim_details.json'),
                JSON.stringify({
                    claim_id: claimId,
                    active_did: activeDid,
                    error: error.message || 'Unknown error',
                    response: error.response?.data
                }, null, 2)
            );
        } catch (writeErr) {
            console.error('Error saving claim error:', writeErr);
        }
        throw error;
    }
}

/**
 * Generate test environment data for deeplink testing
 */
async function generateTestEnv(
    identity: IIdentifier,
    jwtToken: string,
    apiServer: string = API_SERVER
): Promise<void> {
    // Create test data structure
    const testEnvData = {
        CONTACT1_DID: identity.did,
        CONTACT1_KEY: identity.keys[0].privateKeyHex,
        CONTACT2_DID: `did:ethr:${Wallet.createRandom().address}`, // Generate random DID for contact 2
        CONTACT2_KEY: Wallet.createRandom().privateKey.substring(2), // Remove '0x'
        ISSUER_DID: ENDORSER_DID,
        ISSUER_KEY: ENDORSER_PRIVATE_KEY,
        TEST_JWT: jwtToken,
        API_SERVER: apiServer
    };

    // Write test environment variables
    const envContent = Object.entries(testEnvData)
        .map(([key, value]) => `export ${key}="${value}"`)
        .join('\n');
    
    await writeFile(
        join(GENERATED_DIR, 'test-env.sh'),
        envContent + '\n',
        'utf-8'
    );

    // Write test contacts data
    const contactsData = {
        contacts: [
            { did: testEnvData.CONTACT1_DID, name: 'Test Contact 1' },
            { did: testEnvData.CONTACT2_DID, name: 'Test Contact 2' }
        ]
    };

    await writeFile(
        join(GENERATED_DIR, 'contacts.json'),
        JSON.stringify(contactsData, null, 2),
        'utf-8'
    );
}

/**
 * Main execution flow
 */
async function main() {
    // Initialize directories first
    await initializeDirectories();
    
    try {
        // Create a new DID
        const identity = await initializeAccount();
        const activeDid = identity.did;
        const privateKeyHex = identity.keys[0].privateKeyHex;

        // Register the DID
        const result = await register(activeDid, privateKeyHex);
        console.log("Registration result:", result);

        // If registration was successful, fetch the claim details
        if (result.success && result.claimId) {
            console.log("\nFetching claim details...");
            const claimDetails = await fetchClaim(
                result.claimId,
                activeDid,
                privateKeyHex
            );
            console.log("\nClaim Details:");
            console.log(JSON.stringify(claimDetails, null, 2));

            // Generate test environment data
            await generateTestEnv(identity, result.claimId);
        }
    } catch (error: any) {
        console.error("Error:", error.message);
    }
}

// Run if called directly
if (require.main === module) {
    main().catch(console.error);
}

export {
    generateMnemonic,
    deriveAddress,
    newIdentifier,
    initializeAccount,
    createEndorserJwt,
    register,
    fetchClaim,
    generateTestEnv
}; 