"""Backlog management with atomic operations and file locking."""

import json
import os
import tempfile
import threading
import fcntl
from datetime import datetime
from pathlib import Path
from typing import Optional, Tuple

from state.schema import validate_lead

_backlog_lock = threading.RLock()


DEFAULT_BACKLOG_PATH = "./data/backlog.json"


def _get_backlog_path() -> Path:
    """Get backlog file path from env var or default."""
    path = os.environ.get("ABSEL_BACKLOG_PATH", DEFAULT_BACKLOG_PATH)
    return Path(path)


def load_backlog() -> list:
    """Load backlog with shared lock. Returns empty list if file doesn't exist."""
    backlog_path = _get_backlog_path()
    
    if not backlog_path.exists():
        return []
    
    try:
        backlog_path.parent.mkdir(parents=True, exist_ok=True)
        
        with open(backlog_path, 'r') as f:
            fcntl.flock(f.fileno(), fcntl.LOCK_SH)
            try:
                content = f.read()
                if not content.strip():
                    return []
                return json.loads(content)
            finally:
                fcntl.flock(f.fileno(), fcntl.LOCK_UN)
    except (json.JSONDecodeError, IOError) as e:
        raise RuntimeError(f"Failed to load backlog from {backlog_path}: {e}")


def save_backlog(backlog: list) -> None:
    """Save backlog with exclusive lock. Atomic write via temp file + rename."""
    backlog_path = _get_backlog_path()
    backlog_path.parent.mkdir(parents=True, exist_ok=True)

    with _backlog_lock:
        try:
            fd, temp_path = tempfile.mkstemp(
                suffix='.json',
                dir=backlog_path.parent
            )

            try:
                with os.fdopen(fd, 'w') as f:
                    fcntl.flock(f.fileno(), fcntl.LOCK_EX)
                    try:
                        json.dump(backlog, f, indent=2)
                        f.flush()
                        os.fsync(f.fileno())
                    finally:
                        fcntl.flock(f.fileno(), fcntl.LOCK_UN)

                os.rename(temp_path, backlog_path)
            except:
                if os.path.exists(temp_path):
                    os.unlink(temp_path)
                raise

        except (IOError, OSError) as e:
            raise RuntimeError(f"Failed to save backlog to {backlog_path}: {e}")


def get_lead(lead_id: str) -> Optional[dict]:
    """Return the lead with matching id, or None if not found."""
    backlog = load_backlog()
    for lead in backlog:
        if lead.get("id") == lead_id:
            return lead
    return None


def get_leads_by_status(status: str) -> list:
    """Return all leads with a given lead_status value."""
    backlog = load_backlog()
    return [lead for lead in backlog if lead.get("lead_status") == status]


def get_leads_by_stage(temporal_category: str, draft_status: str) -> list:
    """Return leads matching a temporal_category and draft_status combination.

    Args:
        temporal_category: "fresh", "recent", or "historical"
        draft_status: "none", "drafted", or "sent"

    Returns:
        List of leads matching both filters.
    """
    backlog = load_backlog()
    return [
        lead for lead in backlog
        if lead.get("temporal_category") == temporal_category
        and lead.get("draft_status", "none") == draft_status
    ]


def count_by_stage() -> dict:
    """Count leads in each temporal_category x draft_status bucket.

    Returns:
        Dict like {"fresh_none": 12, "fresh_drafted": 5, "historical_none": 340, ...}
    """
    backlog = load_backlog()
    counts = {}
    for temporal in ["fresh", "recent", "historical"]:
        for draft in ["none", "drafted", "sent"]:
            key = f"{temporal}_{draft}"
            counts[key] = sum(
                1 for lead in backlog
                if lead.get("temporal_category") == temporal
                and lead.get("draft_status", "none") == draft
            )
    return counts


def add_lead(lead: dict) -> None:
    """Validate and append a lead to backlog. Raises ValueError if validation fails."""
    is_valid, errors = validate_lead(lead)
    if not is_valid:
        raise ValueError(f"Lead validation failed: {errors}")

    with _backlog_lock:
        backlog = load_backlog()
        backlog.append(lead)
        save_backlog(backlog)


def update_lead(lead_id: str, fields: dict) -> None:
    """Update specific fields on a lead by id. Atomic."""
    with _backlog_lock:
        backlog = load_backlog()

        for i, lead in enumerate(backlog):
            if lead.get("id") == lead_id:
                lead.update(fields)
                lead["last_updated"] = fields.get("last_updated", lead.get("last_updated"))

                is_valid, errors = validate_lead(lead)
                if not is_valid:
                    raise ValueError(f"Lead validation failed after update: {errors}")

                backlog[i] = lead
                save_backlog(backlog)
                return

    raise KeyError(f"Lead with id {lead_id} not found")


def burn_lead(lead_id: str, reason: str = "") -> dict:
    """Permanently mark a lead as burned — no further contact ever.

    Sets lead_status to "burned", records reason and timestamp.
    Burned leads are excluded from all pipeline operations.

    Args:
        lead_id: Lead to burn
        reason: Why the lead was burned (e.g. "remove me from your list")

    Returns:
        The updated lead dict

    Raises:
        KeyError: If lead not found
    """
    lead = get_lead(lead_id)
    if not lead:
        raise KeyError(f"Lead {lead_id} not found")

    now = datetime.utcnow().isoformat() + "Z"

    update_lead(lead_id, {
        "lead_status": "burned",
        "burn_reason": reason,
        "burned_at": now,
        "last_updated": now,
    })

    # Auto-populate suppression registry for opt-out burns
    if "opt_out" in reason.lower():
        from state.suppressions import suppress_email
        email = lead.get("email", "")
        if email:
            suppress_email(email, reason=reason, source_lead_id=lead_id)

    return get_lead(lead_id)


def pause_lead(lead_id: str, reason: str = "") -> dict:
    """Pause a lead — stops touch clock until explicitly unpaused.

    Sets lead_status to "paused", records reason and timestamp.
    Paused leads are excluded from pipeline selection.

    Args:
        lead_id: Lead to pause
        reason: Why the lead was paused

    Returns:
        The updated lead dict

    Raises:
        KeyError: If lead not found
    """
    lead = get_lead(lead_id)
    if not lead:
        raise KeyError(f"Lead {lead_id} not found")

    now = datetime.utcnow().isoformat() + "Z"

    update_lead(lead_id, {
        "lead_status": "paused",
        "paused_at": now,
        "paused_reason": reason,
        "last_updated": now,
    })

    return get_lead(lead_id)


def unpause_lead(lead_id: str) -> dict:
    """Unpause a lead — resumes touch clock and returns to active status.

    Args:
        lead_id: Lead to unpause

    Returns:
        The updated lead dict

    Raises:
        KeyError: If lead not found
        ValueError: If lead is not currently paused
    """
    lead = get_lead(lead_id)
    if not lead:
        raise KeyError(f"Lead {lead_id} not found")

    if lead.get("lead_status") != "paused":
        raise ValueError(f"Lead {lead_id} is not paused (status: {lead.get('lead_status')})")

    now = datetime.utcnow().isoformat() + "Z"

    update_lead(lead_id, {
        "lead_status": "active",
        "last_updated": now,
    })

    return get_lead(lead_id)


def count_by_status() -> dict:
    """Count leads by lead_status.

    Returns:
        Dict like {"active": 500, "burned": 12, "paused": 3, ...}
    """
    backlog = load_backlog()
    counts = {}
    for lead in backlog:
        status = lead.get("lead_status", "unknown")
        counts[status] = counts.get(status, 0) + 1
    return counts


def get_drafted_leads(category: Optional[str] = None) -> list:
    """Return all leads with draft_status == 'drafted', optionally filtered by temporal_category.

    Args:
        category: Optional temporal_category filter ("fresh", "recent", "historical")

    Returns:
        List of drafted leads sorted by drafted_at descending.
    """
    backlog = load_backlog()
    leads = [
        lead for lead in backlog
        if lead.get("draft_status") == "drafted"
        and lead.get("lead_status", "active") in ("active",)
    ]

    if category:
        leads = [l for l in leads if l.get("temporal_category") == category]

    leads.sort(key=lambda x: x.get("drafted_at", ""), reverse=True)
    return leads


def clear_draft(lead_id: str) -> dict:
    """Remove draft from a lead, returning it to undrafted state.

    Clears draft_subject, draft_body, draft_anchor, draft_scenario,
    draft_model, drafted_at, draft_status, reviewed_at.
    Sets draft_status to "none".

    Args:
        lead_id: Lead to clear draft from

    Returns:
        The updated lead dict

    Raises:
        KeyError: If lead not found
    """
    now = datetime.utcnow().isoformat() + "Z"

    update_lead(lead_id, {
        "draft_subject": "",
        "draft_body": "",
        "draft_anchor": "",
        "draft_scenario": "",
        "draft_model": "",
        "drafted_at": "",
        "draft_status": "none",
        "reviewed_at": "",
        "last_updated": now,
    })

    return get_lead(lead_id)


def approve_lead(lead_id: str) -> dict:
    """Mark a lead as reviewed and cleared for sending.

    Sets reviewed_at timestamp. Does not change draft or status.

    Args:
        lead_id: Lead to approve

    Returns:
        The updated lead dict

    Raises:
        KeyError: If lead not found
    """
    now = datetime.utcnow().isoformat() + "Z"

    update_lead(lead_id, {
        "reviewed_at": now,
        "last_updated": now,
    })

    return get_lead(lead_id)


def increment_regen_count(lead_id: str) -> int:
    """Increment and return the regen_count for a lead.

    Args:
        lead_id: Lead to increment

    Returns:
        The new regen_count value

    Raises:
        KeyError: If lead not found
    """
    lead = get_lead(lead_id)
    if not lead:
        raise KeyError(f"Lead {lead_id} not found")

    current = lead.get("regen_count", 0)
    new_count = current + 1

    now = datetime.utcnow().isoformat() + "Z"
    update_lead(lead_id, {
        "regen_count": new_count,
        "reviewed_at": "",
        "last_updated": now,
    })

    return new_count