"""
AWS Provisioning Provider
Creates IAM access keys and STS temporary credentials
Uses native SigV4 signing with hmac/hashlib
"""

import hashlib
import hmac
import json
import re
import urllib.request
import urllib.error
import urllib.parse
from datetime import datetime, timezone
from typing import Any, Optional

from id_wispera.provisioning.provider import ProvisioningProviderBase
from id_wispera.provisioning.types import (
    ProviderCapabilities,
    ProvisioningRequest,
    ProvisioningResult,
    RotationResult,
)


# ============================================================================
# AWS SigV4 Signing Utilities
# ============================================================================

def _hmac_sha256(key: bytes, message: str) -> bytes:
    """Compute HMAC-SHA256."""
    return hmac.new(key, message.encode("utf-8"), hashlib.sha256).digest()


def _sha256(message: str) -> str:
    """Compute SHA-256 hex digest."""
    return hashlib.sha256(message.encode("utf-8")).hexdigest()


def _get_signature_key(
    secret_key: str,
    date_stamp: str,
    region: str,
    service: str,
) -> bytes:
    """Derive the AWS SigV4 signing key."""
    k_date = _hmac_sha256(f"AWS4{secret_key}".encode("utf-8"), date_stamp)
    k_region = _hmac_sha256(k_date, region)
    k_service = _hmac_sha256(k_region, service)
    k_signing = _hmac_sha256(k_service, "aws4_request")
    return k_signing


def _sign_request(
    *,
    method: str,
    host: str,
    path: str,
    body: str,
    service: str,
    region: str,
    access_key_id: str,
    secret_access_key: str,
) -> dict[str, str]:
    """
    Sign an AWS API request using SigV4.

    Returns:
        Dict of signed headers
    """
    now = datetime.now(timezone.utc)
    amz_date = now.strftime("%Y%m%dT%H%M%SZ")
    date_stamp = now.strftime("%Y%m%d")

    payload_hash = _sha256(body)

    canonical_headers = (
        f"content-type:application/x-www-form-urlencoded\n"
        f"host:{host}\n"
        f"x-amz-date:{amz_date}\n"
    )
    signed_headers_list = "content-type;host;x-amz-date"

    canonical_request = "\n".join([
        method,
        path,
        "",  # query string (empty for POST)
        canonical_headers,
        signed_headers_list,
        payload_hash,
    ])

    credential_scope = f"{date_stamp}/{region}/{service}/aws4_request"
    string_to_sign = "\n".join([
        "AWS4-HMAC-SHA256",
        amz_date,
        credential_scope,
        _sha256(canonical_request),
    ])

    signing_key = _get_signature_key(secret_access_key, date_stamp, region, service)
    signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()

    authorization = (
        f"AWS4-HMAC-SHA256 "
        f"Credential={access_key_id}/{credential_scope}, "
        f"SignedHeaders={signed_headers_list}, "
        f"Signature={signature}"
    )

    return {
        "Content-Type": "application/x-www-form-urlencoded",
        "Host": host,
        "X-Amz-Date": amz_date,
        "Authorization": authorization,
    }


def _parse_xml_value(xml: str, tag: str) -> Optional[str]:
    """Parse a simple XML value by tag name."""
    match = re.search(f"<{tag}>([^<]*)</{tag}>", xml)
    return match.group(1) if match else None


def _aws_request(
    *,
    host: str,
    body: str,
    service: str,
    region: str,
    access_key_id: str,
    secret_access_key: str,
) -> tuple[int, str]:
    """
    Make a signed AWS API request.

    Returns:
        Tuple of (status_code, response_body_text)
    """
    headers = _sign_request(
        method="POST",
        host=host,
        path="/",
        body=body,
        service=service,
        region=region,
        access_key_id=access_key_id,
        secret_access_key=secret_access_key,
    )

    req = urllib.request.Request(
        f"https://{host}/",
        data=body.encode("utf-8"),
        headers=headers,
        method="POST",
    )

    try:
        with urllib.request.urlopen(req) as response:
            return response.status, response.read().decode("utf-8")
    except urllib.error.HTTPError as e:
        error_body = e.read().decode("utf-8") if e.fp else ""
        return e.code, error_body


class AWSProvider(ProvisioningProviderBase):
    """AWS provisioning provider for IAM access keys and STS credentials."""

    @property
    def provider(self) -> str:
        return "aws"

    @property
    def capabilities(self) -> ProviderCapabilities:
        return ProviderCapabilities(
            can_create=True,
            can_configure_permissions=True,
            can_rotate=True,
            can_revoke=True,
            can_list=True,
            supports_expiry=True,
            supports_scoped_keys=True,
        )

    def _get_aws_auth(self) -> tuple[str, str, str]:
        """
        Get AWS credentials from auth config.

        Returns:
            Tuple of (access_key_id, secret_access_key, region)
        """
        if not self._auth or self._auth.get("type") != "aws-sigv4":
            raise ValueError("AWS provider requires aws-sigv4 authentication")
        return (
            self._auth["accessKeyId"],
            self._auth["secretAccessKey"],
            self._auth.get("region", "us-east-1"),
        )

    def provision(self, request: ProvisioningRequest) -> ProvisioningResult:
        """Provision a new AWS credential (IAM key or STS temporary credential)."""
        self._ensure_initialized()

        config = request.config
        if config.get("provider") != "aws":
            return ProvisioningResult(
                success=False,
                error={
                    "code": "INVALID_CONFIG",
                    "message": f"Expected aws config, got {config.get('provider')}",
                },
            )

        if not self._auth or self._auth.get("type") != "aws-sigv4":
            return ProvisioningResult(
                success=False,
                error={
                    "code": "INVALID_AUTH",
                    "message": "AWS provider requires aws-sigv4 authentication",
                },
            )

        try:
            if config.get("roleArn"):
                return self._assume_role(request, config)
            else:
                return self._create_access_key(request, config)
        except Exception as e:
            return ProvisioningResult(
                success=False,
                error={
                    "code": "PROVISIONING_FAILED",
                    "message": f"AWS provisioning failed: {str(e)}",
                },
            )

    def list_keys(self) -> list[dict[str, Any]]:
        """List existing AWS IAM access keys."""
        self._ensure_initialized()
        access_key_id, secret_access_key, region = self._get_aws_auth()

        body = "Action=ListAccessKeys&Version=2010-05-08"
        status, xml = _aws_request(
            host="iam.amazonaws.com",
            body=body,
            service="iam",
            region=region,
            access_key_id=access_key_id,
            secret_access_key=secret_access_key,
        )

        if status < 200 or status >= 300:
            raise RuntimeError(f"Failed to list AWS keys: {status} {xml}")

        keys: list[dict[str, Any]] = []
        for member_match in re.finditer(r"<member>([\s\S]*?)</member>", xml):
            member_xml = member_match.group(1)
            key_id = _parse_xml_value(member_xml, "AccessKeyId")
            user_name = _parse_xml_value(member_xml, "UserName")
            key_status = _parse_xml_value(member_xml, "Status")
            create_date = _parse_xml_value(member_xml, "CreateDate")

            if key_id:
                keys.append({
                    "id": key_id,
                    "name": user_name or key_id,
                    "createdAt": create_date or datetime.utcnow().isoformat(),
                    "status": (key_status or "Active").lower(),
                })

        return keys

    def rotate(self, existing_key_id: str, request: ProvisioningRequest) -> RotationResult:
        """Rotate an AWS IAM access key."""
        new_credential = self.provision(request)

        return RotationResult(
            new_credential=new_credential,
            old_key_id=existing_key_id,
            recommended_revocation_delay=300,  # 5 minutes for AWS propagation
        )

    def revoke(self, key_id: str) -> dict[str, Any]:
        """Revoke (delete) an AWS IAM access key."""
        self._ensure_initialized()

        try:
            access_key_id, secret_access_key, region = self._get_aws_auth()
            body = (
                f"Action=DeleteAccessKey"
                f"&AccessKeyId={urllib.parse.quote(key_id, safe='')}"
                f"&Version=2010-05-08"
            )

            status, xml = _aws_request(
                host="iam.amazonaws.com",
                body=body,
                service="iam",
                region=region,
                access_key_id=access_key_id,
                secret_access_key=secret_access_key,
            )

            if status < 200 or status >= 300:
                return {"success": False, "error": f"Failed to delete key: {status} {xml}"}

            return {"success": True}
        except Exception as e:
            return {"success": False, "error": str(e)}

    def validate_auth(self) -> dict[str, Any]:
        """Validate AWS credentials using GetCallerIdentity."""
        self._ensure_initialized()

        try:
            access_key_id, secret_access_key, region = self._get_aws_auth()
            body = "Action=GetCallerIdentity&Version=2011-06-15"

            status, _ = _aws_request(
                host="sts.amazonaws.com",
                body=body,
                service="sts",
                region=region,
                access_key_id=access_key_id,
                secret_access_key=secret_access_key,
            )

            if 200 <= status < 300:
                return {"valid": True}

            return {"valid": False, "error": f"Auth validation failed: {status}"}
        except Exception as e:
            return {"valid": False, "error": str(e)}

    def _create_access_key(
        self,
        request: ProvisioningRequest,
        config: dict[str, Any],
    ) -> ProvisioningResult:
        """Create a long-lived IAM access key."""
        access_key_id, secret_access_key, region = self._get_aws_auth()

        body = "Action=CreateAccessKey&Version=2010-05-08"
        user_name = config.get("userName")
        if user_name:
            body += f"&UserName={urllib.parse.quote(user_name, safe='')}"

        status, xml = _aws_request(
            host="iam.amazonaws.com",
            body=body,
            service="iam",
            region=region,
            access_key_id=access_key_id,
            secret_access_key=secret_access_key,
        )

        if status < 200 or status >= 300:
            error_message = _parse_xml_value(xml, "Message") or f"HTTP {status}"
            error_code = _parse_xml_value(xml, "Code") or f"AWS_{status}"
            return ProvisioningResult(
                success=False,
                error={
                    "code": error_code,
                    "message": error_message,
                    "details": xml,
                },
            )

        new_access_key_id = _parse_xml_value(xml, "AccessKeyId")
        new_secret_access_key = _parse_xml_value(xml, "SecretAccessKey")
        resp_user_name = _parse_xml_value(xml, "UserName")

        if not new_access_key_id or not new_secret_access_key:
            return ProvisioningResult(
                success=False,
                error={
                    "code": "PARSE_ERROR",
                    "message": "Failed to parse access key from AWS response",
                },
            )

        now = datetime.utcnow().isoformat()
        has_elevated = not user_name  # Root-level keys are elevated
        human_owner = request.human_owner
        agent_id = request.agent_id

        return ProvisioningResult(
            success=True,
            credential_value=new_access_key_id,
            secondary_value=new_secret_access_key,
            provider_key_id=new_access_key_id,
            metadata={
                "userName": resp_user_name or user_name,
                "keyType": "long-lived",
            },
            passport_input={
                "name": request.name,
                "credentialType": "api-key",
                "credentialValue": f"{new_access_key_id}:{new_secret_access_key}",
                "visaType": self.map_to_visa_type("aws", has_elevated),
                "issuingAuthority": "AWS IAM",
                "platforms": [self.map_to_platform("aws")],
                "scope": ["iam"],
                "validFrom": now,
                "humanOwner": human_owner,
                "agentId": agent_id,
                "delegationChain": (
                    [h.model_dump(by_alias=True, mode="json") for h in request.delegation_chain]
                    if request.delegation_chain else [
                        {
                            "from": human_owner,
                            "to": agent_id or request.name,
                            "grantedAt": now,
                            "scope": ["iam"],
                            "notes": (
                                f"Provisioned via AWS IAM CreateAccessKey"
                                + (f" (user: {user_name})" if user_name else "")
                            ),
                        }
                    ]
                ),
                "tags": list(request.tags or []) + ["provisioned", "aws", "iam", "long-lived"],
                "notes": (
                    request.notes
                    or f"AWS IAM access key"
                    + (f" for user {user_name}" if user_name else "")
                ),
            },
        )

    def _assume_role(
        self,
        request: ProvisioningRequest,
        config: dict[str, Any],
    ) -> ProvisioningResult:
        """Assume an IAM role via STS for temporary credentials."""
        access_key_id, secret_access_key, region = self._get_aws_auth()
        permissions = request.permissions or {}

        role_arn = config.get("roleArn")
        if not role_arn:
            return ProvisioningResult(
                success=False,
                error={
                    "code": "MISSING_ROLE_ARN",
                    "message": "roleArn is required for STS AssumeRole",
                },
            )

        session_name = config.get("sessionName", "id-wispera-session")
        duration_seconds = config.get("durationSeconds", 3600)

        body = (
            f"Action=AssumeRole&Version=2011-06-15"
            f"&RoleArn={urllib.parse.quote(role_arn, safe='')}"
            f"&RoleSessionName={urllib.parse.quote(session_name, safe='')}"
            f"&DurationSeconds={duration_seconds}"
        )

        external_id = config.get("externalId")
        if external_id:
            body += f"&ExternalId={urllib.parse.quote(external_id, safe='')}"

        session_policy = permissions.get("sessionPolicy")
        if session_policy:
            body += f"&Policy={urllib.parse.quote(session_policy, safe='')}"

        policy_arns = permissions.get("policyArns", [])
        for i, arn in enumerate(policy_arns):
            body += f"&PolicyArns.member.{i + 1}.arn={urllib.parse.quote(arn, safe='')}"

        status, xml = _aws_request(
            host="sts.amazonaws.com",
            body=body,
            service="sts",
            region=region,
            access_key_id=access_key_id,
            secret_access_key=secret_access_key,
        )

        if status < 200 or status >= 300:
            error_message = _parse_xml_value(xml, "Message") or f"HTTP {status}"
            error_code = _parse_xml_value(xml, "Code") or f"AWS_{status}"
            return ProvisioningResult(
                success=False,
                error={
                    "code": error_code,
                    "message": error_message,
                    "details": xml,
                },
            )

        new_access_key_id = _parse_xml_value(xml, "AccessKeyId")
        new_secret_access_key = _parse_xml_value(xml, "SecretAccessKey")
        session_token = _parse_xml_value(xml, "SessionToken")
        expiration = _parse_xml_value(xml, "Expiration")

        if not new_access_key_id or not new_secret_access_key or not session_token:
            return ProvisioningResult(
                success=False,
                error={
                    "code": "PARSE_ERROR",
                    "message": "Failed to parse STS credentials from AWS response",
                },
            )

        now = datetime.utcnow().isoformat()
        expires_at_str = expiration or datetime.utcfromtimestamp(
            datetime.utcnow().timestamp() + duration_seconds
        ).isoformat()
        has_elevated = not session_policy  # Unrestricted sessions are elevated
        scope = ["scoped-session"] if session_policy else ["full-session"]
        human_owner = request.human_owner
        agent_id = request.agent_id

        return ProvisioningResult(
            success=True,
            credential_value=new_access_key_id,
            secondary_value=f"{new_secret_access_key}:{session_token}",
            provider_key_id=new_access_key_id,
            metadata={
                "roleArn": role_arn,
                "sessionName": session_name,
                "sessionToken": session_token,
                "keyType": "temporary",
                "durationSeconds": duration_seconds,
            },
            expires_at=expires_at_str,
            passport_input={
                "name": request.name,
                "credentialType": "api-key",
                "credentialValue": f"{new_access_key_id}:{new_secret_access_key}:{session_token}",
                "visaType": self.map_to_visa_type("aws", has_elevated),
                "issuingAuthority": "AWS STS",
                "platforms": [self.map_to_platform("aws")],
                "scope": scope,
                "validFrom": now,
                "validUntil": expires_at_str,
                "humanOwner": human_owner,
                "agentId": agent_id,
                "delegationChain": (
                    [h.model_dump(by_alias=True, mode="json") for h in request.delegation_chain]
                    if request.delegation_chain else [
                        {
                            "from": human_owner,
                            "to": agent_id or request.name,
                            "grantedAt": now,
                            "scope": scope,
                            "expiresAt": expires_at_str,
                            "notes": f"Provisioned via AWS STS AssumeRole (role: {role_arn})",
                        }
                    ]
                ),
                "tags": list(request.tags or []) + ["provisioned", "aws", "sts", "temporary"],
                "notes": (
                    request.notes
                    or f"AWS STS temporary credentials for role {role_arn} (expires: {expires_at_str})"
                ),
            },
        )
