from __future__ import annotations

import gzip
import hashlib
import json
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional, Tuple, List

from sqlalchemy import delete, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession

from fmatch.saas.models import SfdcImportChunk

CHUNK_TTL_HOURS = 24


def compute_payload_hash(records: list, options: dict) -> str:
    """
    Compute deterministic hash of payload for idempotency.
    Normalizes and sorts to handle key ordering differences.
    """
    normalized = {
        "records": sorted(
            [json.dumps(record, sort_keys=True, default=str) for record in records]
        ),
        "options": json.dumps(options or {}, sort_keys=True, default=str),
    }
    return hashlib.sha256(
        json.dumps(normalized, sort_keys=True).encode("utf-8")
    ).hexdigest()


async def get_or_create_chunk(
    db: AsyncSession,
    *,
    org_id: str,
    import_run_id: str,
    chunk_index: int,
    payload_hash: str,
    idempotency_key: Optional[str] = None,
) -> Tuple[SfdcImportChunk, bool]:
    """
    Get existing chunk or create new one.
    Returns (chunk, is_new).
    """
    expires_at = datetime.now(timezone.utc) + timedelta(hours=CHUNK_TTL_HOURS)
    stmt = (
        pg_insert(SfdcImportChunk)
        .values(
            org_id=org_id,
            import_run_id=import_run_id,
            chunk_index=chunk_index,
            payload_hash=payload_hash,
            idempotency_key=idempotency_key,
            status="pending",
            expires_at=expires_at,
        )
        .on_conflict_do_nothing(constraint="uq_sfdc_chunk_identity")
    )
    result = await db.execute(stmt)
    await db.flush()
    inserted = bool(getattr(result, "rowcount", 0))

    result = await db.execute(
        select(SfdcImportChunk).where(
            SfdcImportChunk.org_id == org_id,
            SfdcImportChunk.import_run_id == import_run_id,
            SfdcImportChunk.chunk_index == chunk_index,
            SfdcImportChunk.payload_hash == payload_hash,
        )
    )
    chunk = result.scalar_one()
    return chunk, inserted


async def get_latest_chunk_for_index(
    db: AsyncSession,
    *,
    org_id: str,
    import_run_id: str,
    chunk_index: int,
) -> Optional[SfdcImportChunk]:
    """Fetch the most recent chunk for a run/index (any payload hash)."""
    result = await db.execute(
        select(SfdcImportChunk)
        .where(SfdcImportChunk.org_id == org_id)
        .where(SfdcImportChunk.import_run_id == import_run_id)
        .where(SfdcImportChunk.chunk_index == chunk_index)
        .where(SfdcImportChunk.expires_at > datetime.now(timezone.utc))
        .order_by(SfdcImportChunk.created_at.desc())
        .limit(1)
    )
    return result.scalar_one_or_none()


async def mark_processing(
    db: AsyncSession,
    chunk: SfdcImportChunk,
    *,
    salesforce_job_id: Optional[str] = None,
    mapping_method: Optional[str] = None,
) -> None:
    """Mark chunk as processing (in-flight)."""
    chunk.status = "processing"
    if salesforce_job_id is not None:
        chunk.salesforce_job_id = salesforce_job_id
    if mapping_method is not None:
        chunk.mapping_method = mapping_method
    chunk.updated_at = datetime.now(timezone.utc)
    await db.flush()


async def mark_completed(
    db: AsyncSession,
    chunk: SfdcImportChunk,
    *,
    results: list,
    issues: list,
    success_count: int,
    mapping_method: Optional[str] = None,
) -> None:
    """Mark chunk as completed with results."""
    chunk.status = "completed"
    chunk.completed_at = datetime.now(timezone.utc)
    if mapping_method is not None:
        chunk.mapping_method = mapping_method
    chunk.result_summary = {
        "success_count": success_count,
        "error_count": len(issues),
        "total": len(results),
    }
    full_results = {"results": results, "issues": issues}
    chunk.result_blob = gzip.compress(
        json.dumps(full_results, default=str).encode("utf-8")
    )
    chunk.updated_at = datetime.now(timezone.utc)
    await db.flush()


async def mark_failed(
    db: AsyncSession,
    chunk: SfdcImportChunk,
    *,
    error: str,
    results: Optional[list] = None,
    issues: Optional[list] = None,
    mapping_method: Optional[str] = None,
    status: Optional[str] = None,
    http_status: Optional[int] = None,
) -> None:
    """Mark chunk as failed."""
    chunk.status = "failed"
    chunk.completed_at = datetime.now(timezone.utc)
    if mapping_method is not None:
        chunk.mapping_method = mapping_method
    chunk.result_summary = {"error": error}
    if status:
        chunk.result_summary["status"] = status
    if http_status:
        chunk.result_summary["http_status"] = http_status
    if results is not None or issues is not None:
        full_results = {"results": results or [], "issues": issues or []}
        chunk.result_blob = gzip.compress(
            json.dumps(full_results, default=str).encode("utf-8")
        )
    chunk.updated_at = datetime.now(timezone.utc)
    await db.flush()


def decompress_results(chunk: SfdcImportChunk) -> Dict[str, Any]:
    """Decompress stored results."""
    if not chunk.result_blob:
        return {"results": [], "issues": []}
    return json.loads(gzip.decompress(chunk.result_blob).decode("utf-8"))


async def get_chunk_by_idempotency_key(
    db: AsyncSession,
    idempotency_key: str,
    org_id: str,
) -> Optional[SfdcImportChunk]:
    """Backward-compat lookup by idempotency key header."""
    result = await db.execute(
        select(SfdcImportChunk)
        .where(SfdcImportChunk.idempotency_key == idempotency_key)
        .where(SfdcImportChunk.org_id == org_id)
        .where(SfdcImportChunk.expires_at > datetime.now(timezone.utc))
        .order_by(SfdcImportChunk.created_at.desc())
        .limit(1)
    )
    return result.scalar_one_or_none()


async def get_pending_bulk_jobs(
    db: AsyncSession,
    org_id: str,
) -> List[SfdcImportChunk]:
    """Get chunks with in-progress bulk jobs for resume."""
    result = await db.execute(
        select(SfdcImportChunk)
        .where(SfdcImportChunk.org_id == org_id)
        .where(SfdcImportChunk.status == "processing")
        .where(SfdcImportChunk.salesforce_job_id.isnot(None))
        .where(SfdcImportChunk.expires_at > datetime.now(timezone.utc))
    )
    return list(result.scalars().all())


async def cleanup_expired_import_chunks(db: AsyncSession) -> int:
    """Delete expired idempotency records. Run daily."""
    result = await db.execute(
        delete(SfdcImportChunk).where(
            SfdcImportChunk.expires_at < datetime.now(timezone.utc)
        )
    )
    await db.commit()
    return result.rowcount or 0
