import argparse
import datetime
import stripe
from stripe.error import InvalidRequestError, APIError
import logging
from dataclasses import dataclass, field
import sys

import os
from dotenv import load_dotenv
import json


from urllib.parse import quote_plus
from django.forms.models import model_to_dict
from django.core.serializers.json import DjangoJSONEncoder

from markdown_pdf import MarkdownPdf, Section

from pypdf import PdfWriter

import concurrent.futures
from typing import Tuple


logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

load_dotenv(dotenv_path=".env")

STRIPE_API_KEY = os.getenv("STRIPE_WRITE_API_KEY")

logging.info(f"Using Stripe WRITE API key: {STRIPE_API_KEY}")

stripe.api_key = STRIPE_API_KEY


project_root = os.path.abspath(
    os.path.join(os.path.dirname(__file__), "../../studio_api")
)
os.chdir(project_root)
sys.path.append(project_root)

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "studio_api.settings")
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = (
    "true"  # https://docs.djangoproject.com/en/4.1/topics/async/#async-safety
)

database_url = f"postgresql://{os.getenv('DATABASE_USER')}:{quote_plus(os.getenv('DATABASE_PASSWORD'))}@{os.getenv('DATABASE_HOST')}/{os.getenv('DATABASE_NAME')}"
replica_database_url = database_url

print("Using database:")
print(f"- host: {os.getenv('DATABASE_HOST')}")
print(f"- username: {os.getenv('DATABASE_USER')}")
print(
    f"- password: {'*' * len(os.getenv('DATABASE_PASSWORD')) if os.getenv('DATABASE_PASSWORD') else 'None'}"
)
print(f"- database: {os.getenv('DATABASE_NAME')}")

if not database_url or not replica_database_url:
    raise ValueError(
        "DATABASE_URL and REPLICA_DATABASE_URL must be set in environment variables"
    )

os.environ["DATABASE_URL"] = database_url
os.environ["REPLICA_DATABASE_URL"] = replica_database_url

import django

django.setup()

from studio_api.bots.models import DiscordInfo, GeneratedClip, UserStats


DISPUTE_TEMPLATE_MARKDOWN = """
We believe this dispute is illegitimate because we collected legitimate payment information from the customer linked to the email address and IP address they used to sign in to our platform, and we provided the customer with the subscription they paid for as well as digital goods via that subscription that they created and have irrevokable access to via downloads.

Our platform Suno is used by customers to create songs.
**Customer "{customer_name}" ({email}) purchased a subscription from us on {subscription_date} for ${subscription_amount}.** 
This is a {subscription_type} giving the ability to create hundreds of songs on our platform.
A typical subscription is $10/month for 500+ song creations or $30/month for 2000+ song creations. 

The customer received the subscription they paid for relevant to the TOS they agree to.
The customer paid for monthly credits to create songs on our platform. 

We validated the customer's credit card and ensured a matching email address to the one they authorized to our service with to ensure the validity of the payment and link the customer's identity between their payment and account. We require that the email address on the Stripe transaction matches an authenticated and verified email address that the customer signed up with via sign in via an OAuth provider like Google or Microsoft.

The customer's credit card was checked with the following results, the best available to us for the given card:
- CVC: {cvc_result}
- Address Line 1: {address_line1_result}
- Address Postal Code: {address_postal_code_result}

We can see they connected from an IP address that matches the stripe payment IP of: {ip_address}

In our terms of service we state that payments are non-refundable:

> "Refunds and Cancellations: Payments made by you hereunder are final and non-refundable, unless otherwise determined by Suno."

"""

SONG_COUNT_TEMPLATE = """
**The customer generated {song_count} songs (2 versions per song) in the month after the charge, which they have irrevokable access to now via downloads. 
They have generated approximately {total_songs} songs in total representing meaningful activity on our platform. 
These song creations cost us money to create by consuming compute resources we pay for.**
Note: This the two are counted slightly differently, so may not match exactly.
"""

DELETED_ACCOUNT_TEMPLATE = """
The customer deleted their Suno account, this is likely a tactic by a fraudster to make it harder for us to analyze their account after the fact.
As a result we'll have limited information about their account and activity, but given the correlation between email address, IP address, and a valid payment method we believe the dispute is illegitimate.
"""


def get_dispute(dispute_id):
    """
    Fetch a specific dispute by its ID.

    Args:
        dispute_id (str): The ID of the dispute to retrieve

    Returns:
        dict: The dispute object if found

    Raises:
        InvalidRequestError: If the dispute doesn't exist or other request issues
    """
    try:
        dispute = stripe.Dispute.retrieve(dispute_id)
        logging.info(f"Successfully retrieved dispute: {dispute_id}")
        return dispute
    except InvalidRequestError as e:
        logging.error(f"Error retrieving dispute {dispute_id}: {str(e)}")
        raise
    except APIError as e:
        logging.error(f"Stripe API error: {str(e)}")
        raise


@dataclass
class DisputeInformation:
    dispute: stripe.Dispute
    customer: stripe.Customer | None = None
    charge: stripe.Charge | None = None
    clips: list[GeneratedClip] = field(default_factory=list)
    cvc_result: str | None = None
    address_line1_result: str | None = None
    address_postal_code_result: str | None = None
    user_stats: UserStats | None = None
    account_was_deleted: bool = False


def get_song_list_markdown(clips: list[GeneratedClip]) -> str:
    base = f"""
Below is a list of songs created by the customer on Suno.com in the relevant subscription period.

### {len(clips)}+ Songs created by the customer within the month after the charge:

Note: Each song listed represents 2 versions of a song provided by Suno to the customer, list of songs may be truncated due to duplicates and PDF size limits.

"""
    original_len = len(clips)
    clips = clips[:100]
    truncated = len(clips) != original_len
    last_clip_string = ""

    for clip in clips:
        title = clip.title or "[No title]"
        clip_time_string = f"- {title} ({clip.created_at.strftime('%m/%d/%y %H:%M')})\n"

        if clip_time_string != last_clip_string:
            base += clip_time_string
            last_clip_string = clip_time_string

    delta = original_len - len(clips)
    if truncated:
        base += f"- {delta} additional songs not listed due to PDF size limits\n"

    return base


def get_user_stats_markdown(user_stats: UserStats) -> str:
    return f"""
We can see the user has activity on our platform as represented by their overall usage statistics:

```
{to_json(user_stats)}
```
"""


def to_json(obj):
    return json.dumps(model_to_dict(obj), indent=2, cls=DjangoJSONEncoder)


def get_dispute_markdown(dispute_info: DisputeInformation) -> str:
    dispute_dollars = int(dispute_info.charge.amount / 100)

    match dispute_dollars:
        case 10:
            subscription_type = "Suno.com monthly pro subscription"
        case 30:
            subscription_type = "Suno.com monthly premier subscription"
        case 4, 8, 16:
            subscription_type = "Suno.com credit top-up (more song creations)"
        case 96:
            subscription_type = "Suno.com yearly pro subscription"
        case 288:
            subscription_type = "Suno.com yearly premier subscription"
        case _:
            subscription_type = "subscription"

    result = DISPUTE_TEMPLATE_MARKDOWN.format(
        customer_name=dispute_info.customer.name,
        email=dispute_info.customer.email,
        subscription_date=datetime.datetime.fromtimestamp(dispute_info.charge.created),
        subscription_amount=dispute_info.charge.amount / 100,
        # song_count=len(dispute_info.clips),
        cvc_result=dispute_info.cvc_result,
        address_line1_result=dispute_info.address_line1_result,
        address_postal_code_result=dispute_info.address_postal_code_result,
        subscription_type=subscription_type,
        ip_address=dispute_info.dispute.evidence.customer_purchase_ip,
        # user_stats=to_json(dispute_info.user_stats),
    )

    if dispute_info.account_was_deleted:
        result += DELETED_ACCOUNT_TEMPLATE
    else:
        result += SONG_COUNT_TEMPLATE.format(
            song_count=len(dispute_info.clips),
            total_songs=dispute_info.user_stats.total_clips
            if dispute_info.user_stats
            else len(dispute_info.clips),
        )

        if dispute_info.user_stats:
            result += get_user_stats_markdown(dispute_info.user_stats)

        result += get_song_list_markdown(dispute_info.clips)

    return result


def sprint(obj):
    print(json.dumps(obj, indent=2))


def dprint(obj):
    print(json.dumps(model_to_dict(obj), indent=2, cls=DjangoJSONEncoder))


def hydrate_dispute_information(dispute: stripe.Dispute):
    # customer = dispute.customer
    # sprint(dispute)
    # print(customer)
    print("Hydrating dispute information")
    charge_id = dispute.charge
    charge = stripe.Charge.retrieve(charge_id)
    # sprint(charge)

    checks = charge.payment_method_details.card.checks

    cvc_result = checks.cvc_check
    address_line1_result = checks.address_line1_check
    address_postal_code_result = checks.address_postal_code_check

    customer_id = charge.customer
    customer = stripe.Customer.retrieve(customer_id)
    # sprint(customer)

    try:
        discord_info = DiscordInfo.objects.get(stripe_customer_id=customer.id)
        # dprint(discord_info)
    except DiscordInfo.DoesNotExist:
        discord_info = None

    charge_created_at_seconds = charge.created
    charge_created_at = datetime.datetime.fromtimestamp(charge_created_at_seconds)
    charge_month_end_approx = charge_created_at + datetime.timedelta(days=32)

    if discord_info:
        clips = GeneratedClip.objects.filter(
            user=discord_info.user,
            created_at__gte=charge_created_at,
            created_at__lte=charge_month_end_approx,
        )
    else:
        clips = []

    print(f"Found {len(clips)} clips in the month after the charge.")
    # for clip in clips:
    # print(f"Clip {clip.id} created at {clip.created_at} -- {clip.title}")

    user_stats = None  # Initialize user_stats with a default value
    try:
        if discord_info:
            user_stats = UserStats.objects.get(user=discord_info.user)
    except UserStats.DoesNotExist:
        print(f"User stats for user {discord_info.user.id} not found")
        pass

    return DisputeInformation(
        dispute=dispute,
        customer=customer,
        charge=charge,
        clips=clips,
        cvc_result=cvc_result,
        address_line1_result=address_line1_result,
        address_postal_code_result=address_postal_code_result,
        user_stats=user_stats,
        account_was_deleted=discord_info is None,
    )


def create_and_save_pdf(markdown_content: str, output_file: str):
    print(f"Creating PDF and saving to {output_file}")
    pdf = MarkdownPdf()
    pdf.add_section(Section(markdown_content, toc=False, paper_size="A4"))
    pdf.save(output_file)
    print(f"PDF saved to {output_file}")
    print("compressing PDF")
    writer = PdfWriter(clone_from=output_file)
    writer.compress_identical_objects(remove_identicals=True, remove_orphans=True)
    for page in writer.pages:
        page.compress_content_streams()
    with open(output_file, "wb") as f:
        writer.write(f)
    print(f"Compressed PDF saved to {output_file}")


def get_usage_markdown(dispute_info: DisputeInformation) -> str:
    base_info = f"""
email_address: {dispute_info.customer.email}
ip_address: {dispute_info.dispute.evidence.customer_purchase_ip}

"""
    if dispute_info.user_stats:
        return base_info + get_user_stats_markdown(dispute_info.user_stats)
    else:
        return base_info


def update_dispute_details(
    dispute_id: str, dispute_info: DisputeInformation, file_path: str, submit: bool
):
    """
    Upload a file to Stripe and update the dispute evidence with that file.

    Args:
        dispute_id (str): The ID of the dispute to update
        file_path (str): Path to the file to upload as evidence
    """
    logging.info(f"Updating dispute {dispute_id} with file {file_path}")

    # First, get the dispute to ensure it exists
    # dispute = get_dispute(dispute_id)

    usage_markdown = get_usage_markdown(dispute_info)

    # Upload the file to Stripe
    try:
        with open(file_path, "rb") as file:
            file_upload = stripe.File.create(
                purpose="dispute_evidence", file=file, file_link_data={"create": True}
            )
        logging.info(f"File uploaded successfully with ID: {file_upload.id}")

        evidence = {
            "uncategorized_file": file_upload.id,
            "access_activity_log": usage_markdown,
        }

        print(f"Evidence being submitted to stripe for dispute {dispute_id}:")
        # print(evidence)

        updated_dispute = stripe.Dispute.modify(
            dispute_id, evidence=evidence, submit=submit
        )

        if submit:
            logging.info(f"Dispute {dispute_id} SUBMITTED with file evidence")
        else:
            logging.info(f"Dispute {dispute_id} updated with file evidence")
        return updated_dispute

    except FileNotFoundError:
        logging.error(f"File not found: {file_path}")
        raise
    except InvalidRequestError as e:
        logging.error(f"Error updating dispute: {str(e)}")
        raise
    except APIError as e:
        logging.error(f"Stripe API error: {str(e)}")
        raise


def submit_dispute(dispute_id: str, output_path: str = None, submit: bool = False):
    dispute = get_dispute(dispute_id)
    dispute_info = hydrate_dispute_information(dispute)
    markdown_content = get_dispute_markdown(dispute_info)
    # print("=" * 100)
    # print("DISPUTE MARKDOWN:")
    # print("=" * 100)
    # print(markdown_content)

    # If no output path specified, create a default one based on dispute ID
    if not output_path:
        output_dir = "dispute_pdfs"
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        output_path = os.path.join(output_dir, f"dispute_{dispute_id}.pdf")

    create_and_save_pdf(markdown_content, output_path)
    update_dispute_details(
        dispute_id, dispute_info, file_path=output_path, submit=submit
    )


def process_disputes_from_file(
    file_path: str, output_dir: str = "dispute_pdfs", submit: bool = False
):
    """Process multiple disputes from a file containing one dispute ID per line using a thread pool."""
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
        print(f"Created output directory: {output_dir}")

    with open(file_path, "r") as f:
        dispute_ids = [line.strip() for line in f if line.strip()]

    print(f"Found {len(dispute_ids)} dispute IDs to process")

    # Use a constant of 5 threads
    max_workers = 5
    print(f"Processing up to {max_workers} disputes concurrently")

    # Process disputes using a thread pool
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        # Submit all tasks and store the futures
        future_to_dispute = {
            executor.submit(
                process_single_dispute,
                dispute_id,
                output_dir,
                submit,
                i,
                len(dispute_ids),
            ): dispute_id
            for i, dispute_id in enumerate(dispute_ids, 1)
        }

        # Process results as they complete
        for future in concurrent.futures.as_completed(future_to_dispute):
            dispute_id = future_to_dispute[future]
            try:
                success, message = future.result()
                if success:
                    print(f"✅ {message}")
                else:
                    print(f"❌ {message}")
            except Exception as e:
                print(f"❌ Error processing dispute {dispute_id}: {str(e)}")


def process_single_dispute(
    dispute_id: str, output_dir: str, submit: bool, index: int, total: int
) -> Tuple[bool, str]:
    """Process a single dispute and return success status and message."""
    output_path = os.path.join(output_dir, f"dispute_{dispute_id}.pdf")
    try:
        print(f"Starting dispute: {dispute_id} ({index}/{total})")
        submit_dispute(dispute_id, output_path, submit)
        return (
            True,
            f"Successfully processed dispute {dispute_id} - {index}/{total} completed",
        )
    except Exception as e:
        return (
            False,
            f"Error processing dispute {dispute_id}: {str(e)} - {index}/{total} attempted",
        )


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Fetch a Stripe dispute")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--dispute_id", help="ID of the dispute to retrieve")
    group.add_argument(
        "--dispute_ids_file", help="File containing one dispute ID per line"
    )

    parser.add_argument("--output", "-o", help="Output PDF file path or directory")
    parser.add_argument(
        "--execute",
        "-e",
        help="Execute the dispute submission, otherwise evidence is staged",
        action="store_true",
        default=False,
    )
    args = parser.parse_args()

    if args.dispute_ids_file:
        output_dir = args.output if args.output else "dispute_pdfs"
        process_disputes_from_file(
            args.dispute_ids_file, output_dir, submit=args.execute
        )
    else:
        submit_dispute(args.dispute_id, args.output, submit=args.execute)
